diff --git a/build/scripts/common.sh b/build/scripts/common.sh index 43f8615d2c..0a59f628c2 100755 --- a/build/scripts/common.sh +++ b/build/scripts/common.sh @@ -8,6 +8,7 @@ if [ -z "${BUILD_OS##*plus*}" ]; then cp -a /code/internal/configs/oidc/* /etc/nginx/oidc/ mkdir -p /etc/nginx/state_files/ mkdir -p /etc/nginx/reporting/ + mkdir -p /etc/nginx/secrets/mgmt/ PLUS=-plus fi diff --git a/charts/nginx-ingress/templates/_helpers.tpl b/charts/nginx-ingress/templates/_helpers.tpl index b44e5df46c..7fe436cca0 100644 --- a/charts/nginx-ingress/templates/_helpers.tpl +++ b/charts/nginx-ingress/templates/_helpers.tpl @@ -116,8 +116,8 @@ Expand the name of the configmap used for NGINX Agent. Expand the name of the mgmt configmap. */}} {{- define "nginx-ingress.mgmtConfigName" -}} -{{- if .Values.controller.mgmt.customConfigMap -}} -{{ .Values.controller.mgmt.customConfigMap }} +{{- if .Values.controller.mgmt.configMapName -}} +{{ .Values.controller.mgmt.configMapName }} {{- else -}} {{- default (printf "%s-mgmt" (include "nginx-ingress.fullname" .)) -}} {{- end -}} @@ -127,7 +127,11 @@ Expand the name of the mgmt configmap. Expand license token secret name. */}} {{- define "nginx-ingress.licenseTokenSecretName" -}} +{{- if hasKey .Values.controller.mgmt "licenseTokenSecretName" -}} {{- .Values.controller.mgmt.licenseTokenSecretName -}} +{{- else }} +{{- fail "Error: When using Nginx Plus, 'controller.mgmt.licenseTokenSecretName' must be set." }} +{{- end -}} {{- end -}} {{/* @@ -393,7 +397,6 @@ volumeMounts: {{ include "nginx-ingress.volumeMountEntries" . }} {{- end -}} {{- end -}} - {{- define "nginx-ingress.volumeMountEntries" -}} {{- if eq (include "nginx-ingress.readOnlyRootFilesystem" .) "true" }} - mountPath: /etc/nginx diff --git a/charts/nginx-ingress/templates/controller-configmap.yaml b/charts/nginx-ingress/templates/controller-configmap.yaml index 98769870de..ac07a721a5 100644 --- a/charts/nginx-ingress/templates/controller-configmap.yaml +++ b/charts/nginx-ingress/templates/controller-configmap.yaml @@ -31,7 +31,7 @@ data: {{ include "nginx-ingress.agentConfiguration" . | indent 4 }} {{- end }} --- -{{- if and .Values.controller.nginxplus (eq (.Values.controller.mgmt.customConfigMap | default "") "") }} +{{- if .Values.controller.nginxplus }} apiVersion: v1 kind: ConfigMap metadata: @@ -44,8 +44,36 @@ metadata: {{ toYaml .Values.controller.config.annotations | indent 4 }} {{- end }} data: - license-token-secret-name: {{ include "nginx-ingress.licenseTokenSecretName" . }} + license-token-secret-name: {{ required "When using Nginx Plus, 'controller.mgmt.licenseTokenSecretName' cannot be empty " (include "nginx-ingress.licenseTokenSecretName" . ) }} +{{- if hasKey .Values.controller.mgmt "sslVerify" }} + ssl-verify: {{ quote .Values.controller.mgmt.sslVerify }} +{{- end }} {{- if hasKey .Values.controller.mgmt "enforceInitialReport" }} enforce-initial-report: {{ quote .Values.controller.mgmt.enforceInitialReport }} {{- end }} +{{- if hasKey .Values.controller.mgmt "usageReport" }} +{{- if hasKey .Values.controller.mgmt.usageReport "endpoint" }} + usage-report-endpoint: {{ quote .Values.controller.mgmt.usageReport.endpoint }} +{{- end }} +{{- if hasKey .Values.controller.mgmt.usageReport "interval" }} + usage-report-interval: {{ quote .Values.controller.mgmt.usageReport.interval }} +{{- end }} +{{- end }} +{{- if hasKey .Values.controller.mgmt "sslTrustedCertificateSecretName" }} + ssl-trusted-certificate-secret-name: {{ quote .Values.controller.mgmt.sslTrustedCertificateSecretName }} +{{- end }} +{{- if hasKey .Values.controller.mgmt "sslCertificateSecretName" }} + ssl-certificate-secret-name: {{ quote .Values.controller.mgmt.sslCertificateSecretName}} +{{- end }} +{{- if hasKey .Values.controller.mgmt "resolver" }} +{{- if hasKey .Values.controller.mgmt.resolver "addresses" }} + resolver-addresses: {{ join "," .Values.controller.mgmt.resolver.addresses | quote }} +{{- end }} +{{- if hasKey .Values.controller.mgmt.resolver "ipv6" }} + resolver-ipv6: {{ quote .Values.controller.mgmt.resolver.ipv6 }} +{{- end }} +{{- if hasKey .Values.controller.mgmt.resolver "valid" }} + resolver-valid: {{ quote .Values.controller.mgmt.resolver.valid }} +{{- end }} +{{- end }} {{- end }} diff --git a/charts/nginx-ingress/values.schema.json b/charts/nginx-ingress/values.schema.json index 28d8e1a856..37fa09e886 100644 --- a/charts/nginx-ingress/values.schema.json +++ b/charts/nginx-ingress/values.schema.json @@ -109,6 +109,42 @@ "license" ] }, + "sslCertificateSecretName": { + "type": "string", + "default": "", + "title": "The sslCertificateSecretName Schema", + "examples": [ + "ssl-certificate" + ] + }, + "usageReport": { + "type": "object", + "default": {}, + "title": "The usageReport Schema", + "properties": { + "endpoint": { + "type": "string", + "title": "The endpoint of the usageReport", + "default": "", + "examples": [ + "", + "product.connect.nginx.com", + "nginx-mgmt.local" + ] + }, + "interval": { + "type": "string", + "pattern": "^[0-9]+[mhd]$", + "default": "1h", + "title": "The usage report interval Schema", + "examples": [ + "1m", + "1h", + "24h" + ] + } + } + }, "enforceInitialReport": { "type": "boolean", "default": false, @@ -117,6 +153,58 @@ true, false ] + }, + "sslVerify": { + "type": "boolean", + "default": true, + "title": "The sslVerify Schema", + "examples": [ + true, + false + ] + }, + "resolver": { + "type": "object", + "default": {}, + "title": "The resolver Schema", + "properties": { + "addresses": { + "type": "array", + "default": [], + "title": "List of resolver addresses/fqdns" + }, + "valid": { + "type": "string", + "pattern": "^[0-9]+[smhdwMy]$", + "title": "A valid nginx time", + "examples": [ + "1m", + "5d" + ] + }, + "ipv6": { + "type": "boolean", + "title": "turn on or off ipv6 support", + "default": false + } + } + }, + "sslTrustedCertificateSecretName": { + "type": "string", + "default": "ssl-trusted-cert", + "title": "The sslTrustedCertificateSecretName Schema", + "examples": [ + "ssl-trusted-cert", + "certificate-secret" + ] + }, + "configMapName": { + "type": "string", + "default": "", + "title": "The configMap Schema", + "examples": [ + "" + ] } }, "examples": [ diff --git a/charts/nginx-ingress/values.yaml b/charts/nginx-ingress/values.yaml index 5bcd9e42c6..9fc4bb2dea 100644 --- a/charts/nginx-ingress/values.yaml +++ b/charts/nginx-ingress/values.yaml @@ -22,6 +22,31 @@ controller: ## Enables the 180-day grace period for sending the initial usage report # enforceInitialReport: false + # usageReport: + # endpoint: "product.connect.nginx.com" # Endpoint for usage report + # interval: 1h + + ## Configures the ssl_verify directive in the mgmt block + # sslVerify: true + + ## Configures the resolver directive in the mgmt block + # resolver: + # ipv6: true + # valid: 1s + # addresses: + # - kube-dns.kube-system.svc.cluster.local + + ## Secret containing TLS client certificate + # sslCertificateSecretName: ssl-certificate # kubernetes.io/tls secret type + + ## Secret containing trusted CA certificate + # sslTrustedCertificateSecretName: ssl-trusted-cert + + # configMapName allows changing the name of the MGMT config map + # the name should not include a namespace + # configMapName: "" + + ## Timeout in milliseconds which the Ingress Controller will wait for a successful NGINX reload after a change or at the initial start. nginxReloadTimeout: 60000 diff --git a/cmd/nginx-ingress/main.go b/cmd/nginx-ingress/main.go index f3a0ad9aca..ccca8288a3 100644 --- a/cmd/nginx-ingress/main.go +++ b/cmd/nginx-ingress/main.go @@ -153,6 +153,15 @@ func main() { if err := processLicenseSecret(kubeClient, nginxManager, mgmtCfgParams, controllerNamespace); err != nil { logEventAndExit(ctx, eventRecorder, pod, secretErrorReason, err) } + + if err := processTrustedCertSecret(kubeClient, nginxManager, mgmtCfgParams, controllerNamespace); err != nil { + logEventAndExit(ctx, eventRecorder, pod, secretErrorReason, err) + } + + if err := processClientAuthSecret(kubeClient, nginxManager, mgmtCfgParams, controllerNamespace); err != nil { + logEventAndExit(ctx, eventRecorder, pod, secretErrorReason, err) + } + } templateExecutor, templateExecutorV2 := createTemplateExecutors(ctx) @@ -204,7 +213,7 @@ func main() { AppProtectBundlePath: appProtectBundlePath, } - mustProcessNginxConfig(staticCfgParams, cfgParams, mgmtCfgParams, templateExecutor, nginxManager) + mustWriteNginxMainConfig(staticCfgParams, cfgParams, mgmtCfgParams, templateExecutor, nginxManager) if *enableTLSPassthrough { var emptyFile []byte @@ -323,6 +332,44 @@ func main() { } } +func processClientAuthSecret(kubeClient *kubernetes.Clientset, nginxManager nginx.Manager, mgmtCfgParams *configs.MGMTConfigParams, controllerNamespace string) error { + if mgmtCfgParams.Secrets.ClientAuth == "" { + return nil + } + + clientAuthSecretNsName := controllerNamespace + "/" + mgmtCfgParams.Secrets.ClientAuth + + secret, err := getAndValidateSecret(kubeClient, clientAuthSecretNsName, api_v1.SecretTypeTLS) + if err != nil { + return fmt.Errorf("error trying to get the client auth secret %v: %w", clientAuthSecretNsName, err) + } + + bytes := configs.GenerateCertAndKeyFileContent(secret) + nginxManager.CreateSecret(fmt.Sprintf("mgmt/%s", configs.ClientAuthCertSecretFileName), bytes, nginx.ReadWriteOnlyFileMode) + return nil +} + +func processTrustedCertSecret(kubeClient *kubernetes.Clientset, nginxManager nginx.Manager, mgmtCfgParams *configs.MGMTConfigParams, controllerNamespace string) error { + if mgmtCfgParams.Secrets.TrustedCert == "" { + return nil + } + + trustedCertSecretNsName := controllerNamespace + "/" + mgmtCfgParams.Secrets.TrustedCert + + secret, err := getAndValidateSecret(kubeClient, trustedCertSecretNsName, secrets.SecretTypeCA) + if err != nil { + return fmt.Errorf("error trying to get the trusted cert secret %v: %w", trustedCertSecretNsName, err) + } + + caBytes, crlBytes := configs.GenerateCAFileContent(secret) + nginxManager.CreateSecret(fmt.Sprintf("mgmt/%s", configs.CACrtKey), caBytes, nginx.ReadWriteOnlyFileMode) + if _, hasCRL := secret.Data[configs.CACrlKey]; hasCRL { + mgmtCfgParams.Secrets.TrustedCRL = secret.Name + nginxManager.CreateSecret(fmt.Sprintf("mgmt/%s", configs.CACrlKey), crlBytes, nginx.ReadWriteOnlyFileMode) + } + return nil +} + func mustCreateConfigAndKubeClient(ctx context.Context) (*rest.Config, *kubernetes.Clientset) { l := nl.LoggerFromContext(ctx) var config *rest.Config @@ -666,9 +713,9 @@ func createGlobalConfigurationValidator() *cr_validation.GlobalConfigurationVali return cr_validation.NewGlobalConfigurationValidator(forbiddenListenerPorts) } -// mustProcessNginxConfig calls internally os.Exit +// mustWriteNginxMainConfig calls internally os.Exit // if can't generate a valid NGINX config. -func mustProcessNginxConfig(staticCfgParams *configs.StaticConfigParams, cfgParams *configs.ConfigParams, mgmtCfgParams *configs.MGMTConfigParams, templateExecutor *version1.TemplateExecutor, nginxManager nginx.Manager) { +func mustWriteNginxMainConfig(staticCfgParams *configs.StaticConfigParams, cfgParams *configs.ConfigParams, mgmtCfgParams *configs.MGMTConfigParams, templateExecutor *version1.TemplateExecutor, nginxManager nginx.Manager) { l := nl.LoggerFromContext(cfgParams.Context) ngxConfig := configs.GenerateNginxMainConfig(staticCfgParams, cfgParams, mgmtCfgParams) content, err := templateExecutor.ExecuteMainConfigTemplate(ngxConfig) @@ -701,7 +748,6 @@ func getSocketClient(sockPath string) *http.Client { } // getAndValidateSecret gets and validates a secret. -// nolint:unparam func getAndValidateSecret(kubeClient *kubernetes.Clientset, secretNsName string, secretType api_v1.SecretType) (secret *api_v1.Secret, err error) { ns, name, err := k8s.ParseNamespaceName(secretNsName) if err != nil { @@ -722,7 +768,13 @@ func getAndValidateSecret(kubeClient *kubernetes.Clientset, secretNsName string, if err != nil { return nil, err } + case secrets.SecretTypeCA: + err = secrets.ValidateCASecret(secret) + if err != nil { + return nil, err + } } + return secret, nil } diff --git a/examples/shared-examples/nginx-plus-secret/README.md b/examples/shared-examples/nginx-plus-secret/README.md new file mode 100644 index 0000000000..9e47f20f56 --- /dev/null +++ b/examples/shared-examples/nginx-plus-secret/README.md @@ -0,0 +1,3 @@ +# NGINX Plus Secret + +Refer to the [Create License Secret](https://docs.nginx.com/nginx-ingress-controller/installation/installing-nic/create-license-secret/) docs to download and create a License Secret diff --git a/examples/shared-examples/nginx-plus-secret/configmap.yaml b/examples/shared-examples/nginx-plus-secret/configmap.yaml new file mode 100644 index 0000000000..8b64038ac4 --- /dev/null +++ b/examples/shared-examples/nginx-plus-secret/configmap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nginx-config-mgmt + namespace: nginx-ingress +data: + license-token-secret-name: "license-token" diff --git a/internal/configs/config_params.go b/internal/configs/config_params.go index 283a10f114..6d427074cf 100644 --- a/internal/configs/config_params.go +++ b/internal/configs/config_params.go @@ -177,13 +177,22 @@ type Listener struct { // MGMTSecrets holds mgmt block secret names type MGMTSecrets struct { - License string + License string + ClientAuth string + TrustedCert string + TrustedCRL string } // MGMTConfigParams holds mgmt block parameters. type MGMTConfigParams struct { Context context.Context + SSLVerify *bool + ResolverAddresses []string + ResolverIPV6 *bool + ResolverValid string EnforceInitialReport *bool + Endpoint string + Interval string Secrets MGMTSecrets } @@ -236,6 +245,7 @@ func NewDefaultConfigParams(ctx context.Context, isPlus bool) *ConfigParams { func NewDefaultMGMTConfigParams(ctx context.Context) *MGMTConfigParams { return &MGMTConfigParams{ Context: ctx, + SSLVerify: nil, EnforceInitialReport: nil, Secrets: MGMTSecrets{}, } diff --git a/internal/configs/configmaps.go b/internal/configs/configmaps.go index 1b339ddeda..ef37831b21 100644 --- a/internal/configs/configmaps.go +++ b/internal/configs/configmaps.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "strings" + "time" v1 "k8s.io/api/core/v1" "k8s.io/client-go/tools/record" @@ -674,6 +675,35 @@ func ParseMGMTConfigMap(ctx context.Context, cfgm *v1.ConfigMap, eventLog record } mgmtCfgParams.Secrets.License = trimmedLicense + if sslVerify, exists, err := GetMapKeyAsBool(cfgm.Data, "ssl-verify", cfgm); exists { + if err != nil { + errorText := fmt.Sprintf("Configmap %s/%s: Invalid value for the ssl-verify key: got %t: %v. Ignoring.", cfgm.GetNamespace(), cfgm.GetName(), sslVerify, err) + nl.Error(l, errorText) + eventLog.Event(cfgm, v1.EventTypeWarning, invalidValueReason, errorText) + configWarnings = true + } else { + mgmtCfgParams.SSLVerify = BoolToPointerBool(sslVerify) + } + } + + if resolverAddresses, exists := GetMapKeyAsStringSlice(cfgm.Data, "resolver-addresses", cfgm, ","); exists { + mgmtCfgParams.ResolverAddresses = resolverAddresses + } + + if resolverIpv6, exists, err := GetMapKeyAsBool(cfgm.Data, "resolver-ipv6", cfgm); exists { + if err != nil { + nl.Error(l, err) + eventLog.Event(cfgm, v1.EventTypeWarning, invalidValueReason, err.Error()) + configWarnings = true + } else { + mgmtCfgParams.ResolverIPV6 = BoolToPointerBool(resolverIpv6) + } + } + + if resolverValid, exists := cfgm.Data["resolver-valid"]; exists { + mgmtCfgParams.ResolverValid = resolverValid + } + if enforceInitialReport, exists, err := GetMapKeyAsBool(cfgm.Data, "enforce-initial-report", cfgm); exists { if err != nil { errorText := fmt.Sprintf("Configmap %s/%s: Invalid value for the enforce-initial-report key: got %t: %v. Ignoring.", cfgm.GetNamespace(), cfgm.GetName(), enforceInitialReport, err) @@ -684,6 +714,36 @@ func ParseMGMTConfigMap(ctx context.Context, cfgm *v1.ConfigMap, eventLog record mgmtCfgParams.EnforceInitialReport = BoolToPointerBool(enforceInitialReport) } } + if endpoint, exists := cfgm.Data["usage-report-endpoint"]; exists { + mgmtCfgParams.Endpoint = strings.TrimSpace(endpoint) + } + if interval, exists := cfgm.Data["usage-report-interval"]; exists { + i := strings.TrimSpace(interval) + t, err := time.ParseDuration(i) + if err != nil { + errorText := fmt.Sprintf("Configmap %s/%s: Invalid value for the interval key: got %q: %v. Ignoring.", cfgm.GetNamespace(), cfgm.GetName(), i, err) + nl.Error(l, errorText) + eventLog.Event(cfgm, v1.EventTypeWarning, invalidValueReason, errorText) + configWarnings = true + } + if t.Seconds() < minimumInterval { + errorText := fmt.Sprintf("Configmap %s/%s: Value too low for the interval key, got: %v, need higher than %ds. Ignoring.", cfgm.GetNamespace(), cfgm.GetName(), i, minimumInterval) + nl.Error(l, errorText) + eventLog.Event(cfgm, v1.EventTypeWarning, invalidValueReason, errorText) + configWarnings = true + mgmtCfgParams.Interval = "" + } else { + mgmtCfgParams.Interval = i + } + + } + if trustedCertSecretName, exists := cfgm.Data["ssl-trusted-certificate-secret-name"]; exists { + mgmtCfgParams.Secrets.TrustedCert = strings.TrimSpace(trustedCertSecretName) + } + + if clientAuthSecretName, exists := cfgm.Data["ssl-certificate-secret-name"]; exists { + mgmtCfgParams.Secrets.ClientAuth = strings.TrimSpace(clientAuthSecretName) + } return mgmtCfgParams, configWarnings, nil } @@ -693,9 +753,19 @@ func GenerateNginxMainConfig(staticCfgParams *StaticConfigParams, config *Config var mgmtConfig version1.MGMTConfig if mgmtCfgParams != nil { mgmtConfig = version1.MGMTConfig{ + SSLVerify: mgmtCfgParams.SSLVerify, + ResolverAddresses: mgmtCfgParams.ResolverAddresses, + ResolverIPV6: mgmtCfgParams.ResolverIPV6, + ResolverValid: mgmtCfgParams.ResolverValid, EnforceInitialReport: mgmtCfgParams.EnforceInitialReport, + Endpoint: mgmtCfgParams.Endpoint, + Interval: mgmtCfgParams.Interval, + TrustedCert: mgmtCfgParams.Secrets.TrustedCert != "", + TrustedCRL: mgmtCfgParams.Secrets.TrustedCRL != "", + ClientAuth: mgmtCfgParams.Secrets.ClientAuth != "", } } + nginxCfg := &version1.MainConfig{ AccessLog: config.MainAccessLog, DefaultServerAccessLogOff: config.DefaultServerAccessLogOff, diff --git a/internal/configs/configurator.go b/internal/configs/configurator.go index 6fc85c841e..f07d55b5de 100644 --- a/internal/configs/configurator.go +++ b/internal/configs/configurator.go @@ -56,6 +56,9 @@ const WildcardSecretFileName = "wildcard" // LicenseSecretFileName is the filename of the Secret for the NGINX PLUS License const LicenseSecretFileName = "license.jwt" +// ClientAuthCertSecretFileName is the filename of the Secret with a TLS cert and a key for the MGMT block for client authentication certificates. +const ClientAuthCertSecretFileName = "client" + // JWTKeyKey is the key of the data field of a Secret where the JWK must be stored. const JWTKeyKey = "jwk" @@ -824,14 +827,12 @@ func generateTLSPassthroughHostsConfig(tlsPassthroughPairs map[string]tlsPassthr return &cfg } -func (cnf *Configurator) addOrUpdateCASecret(secret *api_v1.Secret) string { - name := objectMetaToFileName(&secret.ObjectMeta) +// AddOrUpdateCASecret writes the secret content to disk returning the files added/updated +func (cnf *Configurator) AddOrUpdateCASecret(secret *api_v1.Secret, crtFileName, crlFileName string) string { crtData, crlData := GenerateCAFileContent(secret) - crtSecretName := fmt.Sprintf("%s-%s", name, CACrtKey) - crlSecretName := fmt.Sprintf("%s-%s", name, CACrlKey) - crtFileName := cnf.nginxManager.CreateSecret(crtSecretName, crtData, nginx.ReadWriteOnlyFileMode) - crlFileName := cnf.nginxManager.CreateSecret(crlSecretName, crlData, nginx.ReadWriteOnlyFileMode) - return fmt.Sprintf("%s %s", crtFileName, crlFileName) + crtFilePath := cnf.nginxManager.CreateSecret(crtFileName, crtData, nginx.ReadWriteOnlyFileMode) + crlFilePath := cnf.nginxManager.CreateSecret(crlFileName, crlData, nginx.ReadWriteOnlyFileMode) + return fmt.Sprintf("%s %s", crtFilePath, crlFilePath) } func (cnf *Configurator) addOrUpdateJWKSecret(secret *api_v1.Secret) string { @@ -1999,7 +2000,10 @@ func (cnf *Configurator) AddInternalRouteConfig() error { func (cnf *Configurator) AddOrUpdateSecret(secret *api_v1.Secret) string { switch secret.Type { case secrets.SecretTypeCA: - return cnf.addOrUpdateCASecret(secret) + name := objectMetaToFileName(&secret.ObjectMeta) + crtSecretName := fmt.Sprintf("%s-%s", name, CACrtKey) + crlSecretName := fmt.Sprintf("%s-%s", name, CACrlKey) + return cnf.AddOrUpdateCASecret(secret, crtSecretName, crlSecretName) case secrets.SecretTypeJWK: return cnf.addOrUpdateJWKSecret(secret) case secrets.SecretTypeHtpasswd: diff --git a/internal/configs/configurator_test.go b/internal/configs/configurator_test.go index 62c157eadf..716695d954 100644 --- a/internal/configs/configurator_test.go +++ b/internal/configs/configurator_test.go @@ -101,9 +101,7 @@ func TestConfiguratorUpdatesConfigWithNilCustomMainTemplate(t *testing.T) { t.Parallel() cnf := createTestConfigurator(t) - warnings, err := cnf.UpdateConfig(&ConfigParams{ - MainTemplate: nil, - }, &MGMTConfigParams{}, ExtendedResources{}) + warnings, err := cnf.UpdateConfig(&ConfigParams{MainTemplate: nil}, &MGMTConfigParams{}, ExtendedResources{}) if err != nil { t.Fatal(err) } @@ -141,9 +139,7 @@ func TestConfiguratorUpdatesConfigWithNilCustomIngressTemplate(t *testing.T) { t.Parallel() cnf := createTestConfigurator(t) - warnings, err := cnf.UpdateConfig(&ConfigParams{ - IngressTemplate: nil, - }, &MGMTConfigParams{}, ExtendedResources{}) + warnings, err := cnf.UpdateConfig(&ConfigParams{IngressTemplate: nil}, &MGMTConfigParams{}, ExtendedResources{}) if err != nil { t.Fatal(err) } @@ -159,9 +155,7 @@ func TestConfiguratorUpdatesConfigWithCustomIngressTemplate(t *testing.T) { t.Parallel() cnf := createTestConfigurator(t) - warnings, err := cnf.UpdateConfig(&ConfigParams{ - IngressTemplate: &customTestIngressTemplate, - }, &MGMTConfigParams{}, ExtendedResources{}) + warnings, err := cnf.UpdateConfig(&ConfigParams{IngressTemplate: &customTestIngressTemplate}, &MGMTConfigParams{}, ExtendedResources{}) if err != nil { t.Fatal(err) } diff --git a/internal/configs/version1/config.go b/internal/configs/version1/config.go index 274e4f5636..2302f55ae9 100644 --- a/internal/configs/version1/config.go +++ b/internal/configs/version1/config.go @@ -191,7 +191,16 @@ type Location struct { // MGMTConfig is tbe configuration for the MGMT block. type MGMTConfig struct { + SSLVerify *bool EnforceInitialReport *bool + Endpoint string + Interval string + TrustedCert bool + TrustedCRL bool + ClientAuth bool + ResolverAddresses []string + ResolverIPV6 *bool + ResolverValid string } // MainConfig describe the main NGINX configuration file. diff --git a/internal/configs/version1/nginx-plus.tmpl b/internal/configs/version1/nginx-plus.tmpl index 88ca0b5f0d..b03471eea0 100644 --- a/internal/configs/version1/nginx-plus.tmpl +++ b/internal/configs/version1/nginx-plus.tmpl @@ -152,7 +152,8 @@ http { {{- if .OpenTracingLoadModule}} opentracing_load_tracer {{ .OpenTracingTracer }} /var/lib/nginx/tracer-config.json; {{- end}} - {{ makeResolver .ResolverAddresses .ResolverValid .ResolverIPV6 }} + {{ $resolverIPV6HTTPBool := boolToPointerBool .ResolverIPV6 -}} + {{ makeResolver .ResolverAddresses .ResolverValid $resolverIPV6HTTPBool }} {{if .ResolverTimeout}}resolver_timeout {{.ResolverTimeout}};{{end}} {{- if .OIDC}} @@ -315,7 +316,8 @@ stream { {{- range $value := .StreamSnippets}} {{$value}}{{end}} - {{ makeResolver .ResolverAddresses .ResolverValid .ResolverIPV6 }} + {{ $resolverIPV6StreamBool := boolToPointerBool .ResolverIPV6 -}} + {{ makeResolver .ResolverAddresses .ResolverValid $resolverIPV6StreamBool }} {{if .ResolverTimeout}}resolver_timeout {{.ResolverTimeout}};{{end}} map_hash_max_size {{.MapHashMaxSize}}; @@ -353,7 +355,27 @@ stream { } mgmt { + {{- if or (ne .MGMTConfig.Endpoint "") (ne .MGMTConfig.Interval "") }} + usage_report + {{- if ne .MGMTConfig.Endpoint "" }} endpoint={{ .MGMTConfig.Endpoint }} {{- end }} {{- if ne .MGMTConfig.Interval "" }} interval={{ .MGMTConfig.Interval }} {{- end }}; + {{- end }} license_token {{ printf "%s/license.jwt" .StaticSSLPath }}; enforce_initial_report {{ makeOnOffFromBool .MGMTConfig.EnforceInitialReport}}; + {{ if .MGMTConfig.SSLVerify -}} + ssl_verify {{ makeOnOffFromBool .MGMTConfig.SSLVerify }}; + {{ end -}} + {{ if .MGMTConfig.ResolverAddresses -}} + {{ makeResolver .MGMTConfig.ResolverAddresses .MGMTConfig.ResolverValid .MGMTConfig.ResolverIPV6 }} + {{ end -}} + {{ if .MGMTConfig.TrustedCert -}} + ssl_trusted_certificate {{ printf "%s/mgmt/ca.crt" .StaticSSLPath }}; + {{ end -}} + {{ if .MGMTConfig.TrustedCRL -}} + ssl_crl {{ printf "%s/mgmt/ca.crl" .StaticSSLPath }}; + {{ end -}} + {{ if .MGMTConfig.ClientAuth -}} + ssl_certificate {{ printf "%s/mgmt/client" .StaticSSLPath }}; + ssl_certificate_key {{ printf "%s/mgmt/client" .StaticSSLPath }}; + {{ end -}} deployment_context /etc/nginx/reporting/tracking.info; } diff --git a/internal/configs/version1/template_helper.go b/internal/configs/version1/template_helper.go index 7f79aa84c5..75a99af450 100644 --- a/internal/configs/version1/template_helper.go +++ b/internal/configs/version1/template_helper.go @@ -182,7 +182,7 @@ func generateProxySetHeaders(loc *Location, ingressAnnotations map[string]string return combinedHeaders, nil } -func makeResolver(resolverAddresses []string, resolverValid string, resolverIPV6 bool) string { +func makeResolver(resolverAddresses []string, resolverValid string, resolverIPV6 *bool) string { var builder strings.Builder if len(resolverAddresses) > 0 { builder.WriteString("resolver") @@ -194,7 +194,7 @@ func makeResolver(resolverAddresses []string, resolverValid string, resolverIPV6 builder.WriteString(" valid=") builder.WriteString(resolverValid) } - if !resolverIPV6 { + if resolverIPV6 != nil && !*resolverIPV6 { builder.WriteString(" ipv6=off") } builder.WriteString(";") diff --git a/internal/configs/version1/template_helper_test.go b/internal/configs/version1/template_helper_test.go index 8cc7f94e5e..a472242635 100644 --- a/internal/configs/version1/template_helper_test.go +++ b/internal/configs/version1/template_helper_test.go @@ -838,72 +838,79 @@ func TestMakeResolver(t *testing.T) { name string resolverAddresses []string resolverValid string - resolverIPV6 bool + resolverIPV6 *bool expected string }{ { name: "No addresses", resolverAddresses: []string{}, resolverValid: "", - resolverIPV6: true, + resolverIPV6: boolToPointerBool(true), expected: "", }, { name: "Single address, default options", resolverAddresses: []string{"8.8.8.8"}, resolverValid: "", - resolverIPV6: true, + resolverIPV6: boolToPointerBool(true), expected: "resolver 8.8.8.8;", }, { name: "Multiple addresses, valid time, ipv6 on", resolverAddresses: []string{"8.8.8.8", "8.8.4.4"}, resolverValid: "30s", - resolverIPV6: true, + resolverIPV6: boolToPointerBool(true), expected: "resolver 8.8.8.8 8.8.4.4 valid=30s;", }, { name: "Single address, ipv6 off", resolverAddresses: []string{"8.8.8.8"}, resolverValid: "", - resolverIPV6: false, + resolverIPV6: boolToPointerBool(false), expected: "resolver 8.8.8.8 ipv6=off;", }, { name: "Multiple addresses, valid time, ipv6 off", resolverAddresses: []string{"8.8.8.8", "8.8.4.4"}, resolverValid: "30s", - resolverIPV6: false, + resolverIPV6: boolToPointerBool(false), expected: "resolver 8.8.8.8 8.8.4.4 valid=30s ipv6=off;", }, { name: "No valid time, ipv6 off", resolverAddresses: []string{"8.8.8.8"}, resolverValid: "", - resolverIPV6: false, + resolverIPV6: boolToPointerBool(false), expected: "resolver 8.8.8.8 ipv6=off;", }, { name: "Valid time only", resolverAddresses: []string{"8.8.8.8"}, resolverValid: "10s", - resolverIPV6: true, + resolverIPV6: boolToPointerBool(true), expected: "resolver 8.8.8.8 valid=10s;", }, { name: "IPv6 only", resolverAddresses: []string{"8.8.8.8"}, resolverValid: "", - resolverIPV6: false, + resolverIPV6: boolToPointerBool(false), expected: "resolver 8.8.8.8 ipv6=off;", }, { name: "All options", resolverAddresses: []string{"8.8.8.8", "8.8.4.4", "1.1.1.1"}, resolverValid: "60s", - resolverIPV6: false, + resolverIPV6: boolToPointerBool(false), expected: "resolver 8.8.8.8 8.8.4.4 1.1.1.1 valid=60s ipv6=off;", }, + { + name: "All options, ipv6 nil", + resolverAddresses: []string{"8.8.8.8", "8.8.4.4", "1.1.1.1"}, + resolverValid: "60s", + resolverIPV6: nil, + expected: "resolver 8.8.8.8 8.8.4.4 1.1.1.1 valid=60s;", + }, } for _, tc := range testCases { diff --git a/internal/k8s/controller.go b/internal/k8s/controller.go index 87170d154f..146add5268 100644 --- a/internal/k8s/controller.go +++ b/internal/k8s/controller.go @@ -106,6 +106,8 @@ type specialSecrets struct { defaultServerSecret string wildcardTLSSecret string licenseSecret string + clientAuthSecret string + trustedCertSecret string } type controllerMetadata struct { @@ -250,6 +252,8 @@ func NewLoadBalancerController(input NewLoadBalancerControllerInput) *LoadBalanc } if input.IsNginxPlus { specialSecrets.licenseSecret = fmt.Sprintf("%s/%s", input.ControllerNamespace, input.NginxConfigurator.MgmtCfgParams.Secrets.License) + specialSecrets.clientAuthSecret = fmt.Sprintf("%s/%s", input.ControllerNamespace, input.NginxConfigurator.MgmtCfgParams.Secrets.ClientAuth) + specialSecrets.trustedCertSecret = fmt.Sprintf("%s/%s", input.ControllerNamespace, input.NginxConfigurator.MgmtCfgParams.Secrets.TrustedCert) } lbc := &LoadBalancerController{ client: input.KubeClient, @@ -888,6 +892,16 @@ func (lbc *LoadBalancerController) updateAllConfigs() { if mgmtErr != nil { nl.Errorf(lbc.Logger, "configmap %s/%s: %v", lbc.mgmtConfigMap.GetNamespace(), lbc.mgmtConfigMap.GetName(), mgmtErr) } + // update special CA secret in mgmtConfigParams + if mgmtCfgParams.Secrets.TrustedCert != "" { + secret, err := lbc.client.CoreV1().Secrets(lbc.mgmtConfigMap.GetNamespace()).Get(context.TODO(), mgmtCfgParams.Secrets.TrustedCert, meta_v1.GetOptions{}) + if err != nil { + nl.Errorf(lbc.Logger, "secret %s/%s: %v", lbc.mgmtConfigMap.GetNamespace(), mgmtCfgParams.Secrets.TrustedCert, err) + } + if _, hasCRL := secret.Data[configs.CACrlKey]; hasCRL { + mgmtCfgParams.Secrets.TrustedCRL = secret.Name + } + } } resources := lbc.configuration.GetResources() @@ -1786,6 +1800,10 @@ func (lbc *LoadBalancerController) isSpecialSecret(secretName string) bool { return true case lbc.specialSecrets.licenseSecret: return true + case lbc.specialSecrets.clientAuthSecret: + return true + case lbc.specialSecrets.trustedCertSecret: + return true default: return false } @@ -1837,7 +1855,6 @@ func (lbc *LoadBalancerController) handleSpecialSecretUpdate(secret *api_v1.Secr return } - lbc.writeSpecialSecrets(secret, secretNsName, specialTLSSecretsToUpdate) if ok := lbc.writeSpecialSecrets(secret, secretNsName, specialTLSSecretsToUpdate); !ok { // if not ok bail early return @@ -1853,6 +1870,15 @@ func (lbc *LoadBalancerController) handleSpecialSecretUpdate(secret *api_v1.Secr if ok := lbc.performDynamicSSLReload(secret); !ok { return } + case lbc.specialSecrets.clientAuthSecret: + if ok := lbc.performNGINXReload(secret); !ok { + return + } + case lbc.specialSecrets.trustedCertSecret: + lbc.updateAllConfigs() + if ok := lbc.performNGINXReload(secret); !ok { + return + } } lbc.recorder.Eventf(secret, api_v1.EventTypeNormal, "Updated", "the special Secret %v was updated", secretNsName) @@ -1868,6 +1894,8 @@ func (lbc *LoadBalancerController) writeSpecialSecrets(secret *api_v1.Secret, se lbc.recorder.Eventf(lbc.metadata.pod, api_v1.EventTypeWarning, "UpdatedWithError", "the license Secret %v was updated, but not applied: %v", secretNsName, err) return false } + case secrets.SecretTypeCA: + lbc.configurator.AddOrUpdateCASecret(secret, fmt.Sprintf("mgmt/%s", configs.CACrtKey), fmt.Sprintf("mgmt/%s", configs.CACrlKey)) case api_v1.SecretTypeTLS: lbc.configurator.AddOrUpdateSpecialTLSSecrets(secret, specialTLSSecretsToUpdate) } @@ -1889,6 +1917,17 @@ func (lbc *LoadBalancerController) specialSecretValidation(secretNsName string, return false } } + if secretNsName == lbc.specialSecrets.trustedCertSecret { + err := secrets.ValidateCASecret(secret) + if err != nil { + nl.Errorf(lbc.Logger, "Couldn't validate the special Secret %v: %v", secretNsName, err) + lbc.recorder.Eventf(lbc.metadata.pod, api_v1.EventTypeWarning, "Rejected", "the special Secret %v was rejected, using the previous version: %v", secretNsName, err) + return false + } + } + if secretNsName == lbc.specialSecrets.clientAuthSecret { + lbc.validationTLSSpecialSecret(secret, configs.ClientAuthCertSecretFileName, specialTLSSecretsToUpdate) + } return true } @@ -1903,7 +1942,7 @@ func (lbc *LoadBalancerController) performNGINXReload(secret *api_v1.Secret) boo secretNsName := generateSecretNSName(secret) if err := lbc.configurator.Reload(false); err != nil { nl.Errorf(lbc.Logger, "error when reloading NGINX when updating the special Secrets: %v", err) - lbc.recorder.Eventf(secret, api_v1.EventTypeWarning, "UpdatedWithError", "the special Secret %v was updated, but not applied: %v", secretNsName, err) + lbc.recorder.Eventf(lbc.metadata.pod, api_v1.EventTypeWarning, "UpdatedWithError", "the special Secret %v was updated, but not applied: %v", secretNsName, err) return false } return true diff --git a/site/content/configuration/global-configuration/command-line-arguments.md b/site/content/configuration/global-configuration/command-line-arguments.md index 3436c26a82..05916d5ff7 100644 --- a/site/content/configuration/global-configuration/command-line-arguments.md +++ b/site/content/configuration/global-configuration/command-line-arguments.md @@ -231,6 +231,16 @@ Format: `/` --- +### -mgmt-configmap `` + +The Management ConfigMap resource is used for customizing the NGINX mgmt block. If using NGINX Plus, a Management ConfigMap must be set. If NGINX Ingress Controller is not able to fetch it from Kubernetes API, NGINX Ingress Controller will fail to start. + +Format: `/` + + + +--- + ### -nginx-debug Enable debugging for NGINX. Uses the nginx-debug binary. Requires 'error-log-level: debug' in the ConfigMap. diff --git a/site/content/configuration/global-configuration/mgmt-configmap-resource.md b/site/content/configuration/global-configuration/mgmt-configmap-resource.md new file mode 100644 index 0000000000..20713d3d0d --- /dev/null +++ b/site/content/configuration/global-configuration/mgmt-configmap-resource.md @@ -0,0 +1,49 @@ +--- +docs: DOCS-586 +doctypes: +- '' +title: Management ConfigMap resource +toc: true +weight: 300 +--- + +When using F5 NGINX Ingress Controller with NGINX Plus, it is required to pass a [command line argument]({{< relref "configuration/global-configuration/command-line-arguments" >}}) to NGINX Ingress Controller, `--mgmt-configmap=` which specifies the ConfigMap to use. The minimal required ConfigMap must have a `license-token-secret-name` key. Helm users will not need to create this map or pass the argument, it will be created with a Helm install. + +--- + +1. Create a ConfigMap file with the name *nginx-config-mgmt.yaml* and set the values +that make sense for your setup: + + ```yaml + apiVersion: v1 + kind: ConfigMap + metadata: + name: nginx-config-mgmt + namespace: nginx-ingress + data: + license-token-secret-name: "license-token" + ``` +1. Create a new (or update the existing) ConfigMap resource: + + ```shell + kubectl apply -f nginx-config-mgmt.yaml + ``` + + The [NGINX Management](https://nginx.org/en/docs/ngx_mgmt_module.html) block configuration will be updated. +--- +## Management ConfigMap keys + +{{}} +|ConfigMap Key | Description | Default | +| ---| ---| ---| +|*license-token-secret-name* | Configures the secret used in the [license_token](https://nginx.org/en/docs/ngx_mgmt_module.html#license_token) directive. This key assumes the secret is in the Namespace that NGINX Ingress Controller is deployed in. The secret must be of type `nginx.com/license` with the base64 encoded JWT in the `license.jwt` key. | N/A | +|*ssl-verify* | Configures the [ssl_verify](https://nginx.org/en/docs/ngx_mgmt_module.html#ssl_verify) directive, which enables or disables verification of the usage reporting endpoint certificate. | `true` | +|*enforce-initial-report* | Configures the [enforce_initial_report](https://nginx.org/en/docs/ngx_mgmt_module.html#enforce_initial_report) directive, which enables or disables the 180-day grace period for sending the initial usage report. | `false` | +|*usage-report-endpoint* | Configures the endpoint of the [usage_report](https://nginx.org/en/docs/ngx_mgmt_module.html#usage_report) directive. This is used to configure the endpoint NGINX uses to send usage reports to NIM. | `product.connect.nginx.com` | +|*usage-report-interval* | Configures the interval of the [usage_report](https://nginx.org/en/docs/ngx_mgmt_module.html#usage_report) directive. This specifies the frequency that usage reports are sent. This field takes an [NGINX time](https://nginx.org/en/docs/syntax.html). | `1h` | +|*ssl-trusted-certificate-secret-name* | Configures the secret used to create the file(s) referenced the in [ssl_trusted_certifcate](https://nginx.org/en/docs/ngx_mgmt_module.html#ssl_trusted_certificate), and [ssl_crl](https://nginx.org/en/docs/ngx_mgmt_module.html#ssl_crl) directives. This key assumes the secret is in the Namespace that NGINX Ingress Controller is deployed in. The secret must be of type `nginx.org/ca`, where the `ca.crt` key contains a base64 encoded trusted cert, and the optional `ca.crl` key can contain a base64 encoded CRL. If the optional `ca.crl` key is supplied, it will configure the NGINX `ssl_crl` directive. | N/A | +|*ssl-certificate-secret-name* | Configures the secret used to create the `ssl_certificate` and `ssl_certificate_key` directives. This key assumes the secret is in the Namespace that NGINX Ingress Controller is deployed in. The secret must be of type `kubernetes.io/tls`| N/A | +|*resolver-addresses* | Configures addresses used in the mgmt block [resolver](https://nginx.org/en/docs/ngx_mgmt_module.html#resolver) directive. This field takes a comma separated list of addresses. | N/A | +|*resolver-ipv6* | Configures whether the mgmt block [resolver](https://nginx.org/en/docs/ngx_mgmt_module.html#resolver) directive will look up IPv6 addresses. | `true` | +|*resolver-valid* | Configures an [NGINX time](https://nginx.org/en/docs/syntax.html) that the mgmt block [resolver](https://nginx.org/en/docs/ngx_mgmt_module.html#resolver) directive will override the TTL value of responses from nameservers with. | N/A | +{{}} diff --git a/site/content/includes/installation/download-jwt.md b/site/content/includes/installation/download-jwt.md new file mode 100644 index 0000000000..d89c65a43b --- /dev/null +++ b/site/content/includes/installation/download-jwt.md @@ -0,0 +1,10 @@ +--- +docs: "DOCS-000" +--- + +1. Log in to [MyF5](https://my.f5.com/manage/s/). +2. Go to **My Products & Plans > Subscriptions** to see your active subscriptions. +3. Find your NGINX products or services subscription, and select the **Subscription ID** for details. +4. Download the **JSON Web Token (JWT)** from the subscription page. + +{{< note >}} The Connectivity Stack for Kubernetes JWT does not work with NGINX Plus reporting. A regular NGINX Plus instance JWT must be used. {{< /note >}} diff --git a/site/content/installation/installing-nic/create-license-secret.md b/site/content/installation/installing-nic/create-license-secret.md new file mode 100644 index 0000000000..2a71f2218d --- /dev/null +++ b/site/content/installation/installing-nic/create-license-secret.md @@ -0,0 +1,150 @@ +--- +title: Create a License Secret +toc: true +weight: 100 +docs: DOCS-000 +--- + +This document explains how to create and use a license secret for NGINX Ingress Controller. + +# Overview + +NGINX Plus Ingress Controller requires a valid JSON Web Token (JWT) to download the container image from the F5 registry. From version 4.0.0, this JWT token is also required to run NGINX Plus. + +This requirement is part of F5’s broader licensing program and aligns with industry best practices. The JWT will streamline subscription renewals and usage reporting, helping you manage your NGINX Plus subscription more efficiently. The [telemetry](#telemetry) data we collect helps us improve our products and services to better meet your needs. + +The JWT is required for validating your subscription and reporting telemetry data. For environments connected to the internet, telemetry is automatically sent to F5’s licensing endpoint. In offline environments, telemetry is routed through [NGINX Instance Manager](https://docs.nginx.com/nginx-instance-manager/). By default usage is reported every hour and also whenever NGINX is reloaded. + +{{< note >}} Read the [subscription licenses topic](https://docs.nginx.com/solutions/about-subscription-licenses/#for-internet-connected-environments) for a list of IPs associated with F5's licensing endpoint (`product.connect.nginx.com`). {{}} + +## Set up the JWT + +{{< include "installation/download-jwt.md" >}} + +The JWT needs to be configured before deploying NGINX Ingress Controller. The JWT will be stored in a Kubernetes Secret of type `nginx.com/license`, and can be created with the following command. + +```shell +kubectl create secret generic license-token --from-file=license.jwt= --type=nginx.com/license -n +``` +You can now delete the downloaded `.jwt` file. + +{{< note >}} +The Secret needs to be in the same Namespace as the NGINX Ingress Controller Pod(s). +{{}} + +{{< include "installation/jwt-password-note.md" >}} + +### Use the NGINX Plus license Secret + +If using a name other than the default `license-token`, provide the name of this Secret when installing NGINX Ingress Controller: + +{{}} + +{{%tab name="Helm"%}} + +Specify the Secret name using the `controller.mgmt.licenseTokenSecretName` helm value. + +For detailed guidance on creating the Management block via Helm, refer to the [Helm configuration documentation]({{< relref "installation/installing-nic/installation-with-helm/#configuration" >}}). + +{{% /tab %}} + +{{%tab name="Manifests"%}} + +Specify the Secret name in the `license-token-secret-name` Management ConfigMap key. + +For detailed guidance on creating the Management ConfigMap, refer to the [Management ConfigMap Resource Documentation]({{< relref "configuration/global-configuration/mgmt-configmap-resource/" >}}). + +{{% /tab %}} + +{{}} + +**If you are reporting to the default licensing endpoint, then you can now proceed with [installing NGINX Ingress Controller]({{< relref "installation/installing-nic/" >}}). Otherwise, follow the steps below to configure reporting to NGINX Instance Manager.** + + +### Reports for NGINX Instance Manager {#nim} + +If you are deploying NGINX Ingress Controller in an "air-gapped" environment you will need to report to NGINX Instance Manager (NIM) instead of the default licensing endpoint. + +First, you must specify the endpoint of your NGINX Instance Manager: + +{{}} + +{{%tab name="Helm"%}} + +Specify the endpoint using the `controller.mgmt.usageReport.endpoint` helm value. + +{{% /tab %}} + +{{%tab name="Manifests"%}} + +Specify the endpoint in the `usage-report-endpoint` Management ConfigMap key. + +{{% /tab %}} + +{{}} + +#### Configure SSL certificates and SSL trusted certificates {#nim-cert} + +To configure SSL certificates or SSL trusted certificates, extra steps are necessary. + +To use Client Auth with NGINX Instance Manager, first create a Secret of type `kubernetes.io/tls` in the same namespace as the NGINX Ingress Controller pods. + +```shell +kubectl create secret tls ssl-certificate --cert= --key= -n +``` + +To provide a SSL trusted certificate, and an optional Certificate Revocation List, create a Secret of type `nginx.org/ca` in the Namespace that the NIC Pod(s) are in. + +```shell +kubectl create secret generic ssl-trusted-certificate \ + --from-file=ca.crt= \ + --from-file=ca.crl= \ # optional + --type=nginx.org/ca +``` + +Providing an optional CRL (certificate revocation list) will configure the [`ssl_crl`](https://nginx.org/en/docs/ngx_mgmt_module.html#ssl_crl) directive. + +{{}} + +{{%tab name="Helm"%}} + +Specify the SSL certificate Secret name using the `controller.mgmt.sslCertificateSecretName` Helm value. + +Specify the SSL trusted certificate Secret name using the `controller.mgmt.sslTrustedCertificateSecretName` Helm value. + +{{% /tab %}} + +{{%tab name="Manifests"%}} + +Specify the SSL certificate Secret name in the `ssl-certificate-secret-name` management ConfigMap key. + +Specify the SSL trusted certificate Secret name in the `ssl-trusted-certificate-secret-name` management ConfigMap key. + +{{% /tab %}} + +{{}} + +
+ +**Once these Secrets are created and configured, you can now [install NGINX Ingress Controller ]({{< relref "installation/installing-nic" >}}).** + +## What’s reported and how it’s protected {#telemetry} + +NGINX Plus reports the following data every hour by default: + +- **NGINX version and status**: The version of NGINX Plus running on the instance. +- **Instance UUID**: A unique identifier for each NGINX Plus instance. +- **Traffic data**: + - **Bytes received from and sent to clients**: HTTP and stream traffic volume between clients and NGINX Plus. + - **Bytes received from and sent to upstreams**: HTTP and stream traffic volume between NGINX Plus and upstream servers. + - **Client connections**: The number of accepted client connections (HTTP and stream traffic). + - **Requests handled**: The total number of HTTP requests processed. +- **NGINX uptime**: The number of reloads and worker connections during uptime. +- **Usage report timestamps**: Start and end times for each usage report. +- **Kubernetes node details**: Information about Kubernetes nodes. + +### Security and privacy of reported data + +All communication between your NGINX Plus instances, NGINX Instance Manager, and F5’s licensing endpoint (`product.connect.nginx.com`) is protected using **SSL/TLS** encryption. + +Only **operational metrics** are reported — no **personally identifiable information (PII)** or **sensitive customer data** is transmitted. diff --git a/site/content/installation/installing-nic/installation-with-helm.md b/site/content/installation/installing-nic/installation-with-helm.md index 370a9d84cd..b416838fbb 100644 --- a/site/content/installation/installing-nic/installation-with-helm.md +++ b/site/content/installation/installing-nic/installation-with-helm.md @@ -14,6 +14,7 @@ This document explains how to install F5 NGINX Ingress Controller using [Helm](h - A [Kubernetes Version Supported by NGINX Ingress Controller](https://docs.nginx.com/nginx-ingress-controller/technical-specifications/#supported-kubernetes-versions) - Helm 3.0+. - If you’d like to use NGINX Plus: + - Get the NGINX Ingress Controller JWT and [create a license secret]({{< relref "installation/installing-nic/create-license-secret/" >}}). - Download the image using your NGINX Ingress Controller subscription certificate and key. View the [Get NGINX Ingress Controller from the F5 Registry]({{< relref "installation/nic-images/get-registry-image.md" >}}) topic. - The [Get the NGINX Ingress Controller image with JWT]({{< relref "installation/nic-images/get-image-using-jwt.md" >}}) topic describes how to use your subscription JWT token to get the image. - The [Build NGINX Ingress Controller]({{< relref "installation/build-nginx-ingress-controller.md" >}}) topic explains how to push an image to a private Docker registry. @@ -305,6 +306,17 @@ The following tables lists the configurable parameters of the NGINX Ingress Cont | **controller.kind** | The kind of the Ingress Controller installation - deployment or daemonset. | deployment | | **controller.annotations** | Allows for setting of `annotations` for deployment or daemonset. | {} | | **controller.nginxplus** | Deploys the Ingress Controller for NGINX Plus. | false | +| **controller.mgmt.licenseTokenSecretName** | Configures the secret used in the [license_token](https://nginx.org/en/docs/ngx_mgmt_module.html#license_token) directive. This key assumes the secret is in the Namespace that NGINX Ingress Controller is deployed in. The secret must be of type `nginx.com/license` with the base64 encoded JWT in the `license.jwt` key. | license-token | +| **controller.mgmt.enforceInitialReport** | Configures the [enforce_initial_report](https://nginx.org/en/docs/ngx_mgmt_module.html#enforce_initial_report) directive, which enables or disables the 180-day grace period for sending the initial usage report. | false | +| **controller.mgmt.usageReport.endpoint** | Configures the endpoint of the [usage_report](https://nginx.org/en/docs/ngx_mgmt_module.html#usage_report) directive. This is used to configure the endpoint NGINX uses to send usage reports to NIM. | product.connect.nginx.com | +| **controller.mgmt.usageReport.interval** | Configures the interval of the [usage_report](https://nginx.org/en/docs/ngx_mgmt_module.html#usage_report) directive. This specifies the frequency that usage reports are sent. This field takes an [NGINX time](https://nginx.org/en/docs/syntax.html). | 1h | +| **controller.mgmt.sslVerify** | Configures the [ssl_verify](https://nginx.org/en/docs/ngx_mgmt_module.html#ssl_verify) directive, which enables or disables verification of the usage reporting endpoint certificate. | true | +| **controller.mgmt.resolver.ipv6** | Configures whether the mgmt block [resolver](https://nginx.org/en/docs/ngx_mgmt_module.html#resolver) directive will look up IPv6 addresses. | true | +| **controller.mgmt.resolver.valid** | Configures an [NGINX time](https://nginx.org/en/docs/syntax.html) that the mgmt block [resolver](https://nginx.org/en/docs/ngx_mgmt_module.html#resolver) directive will override the TTL value of responses from nameservers with. | N/A | +| **controller.mgmt.resolver.addresses** | Configures addresses used in the mgmt block [resolver](https://nginx.org/en/docs/ngx_mgmt_module.html#resolver) directive. This field takes a list of addresses. | N/A | +| **controller.mgmt.sslCertificateSecretName** | Configures the secret used to create the `ssl_certificate` and `ssl_certificate_key` directives. This key assumes the secret is in the Namespace that NGINX Ingress Controller is deployed in. The secret must be of type `kubernetes.io/tls` | N/A | +| **controller.mgmt.sslTrustedCertificateSecretName** | Configures the secret used to create the file(s) referenced the in [ssl_trusted_certifcate](https://nginx.org/en/docs/ngx_mgmt_module.html#ssl_trusted_certificate), and [ssl_crl](https://nginx.org/en/docs/ngx_mgmt_module.html#ssl_crl) directives. This key assumes the secret is in the Namespace that NGINX Ingress Controller is deployed in. The secret must be of type `nginx.org/ca`, where the `ca.crt` key contains a base64 encoded trusted cert, and the optional `ca.crl` key can contain a base64 encoded CRL. If the optional `ca.crl` key is supplied, it will configure the NGINX `ssl_crl` directive. | N/A | +| **controller.mgmt.configMapName** | Allows changing the name of the MGMT config map. The name should not include a namespace| Autogenerated | | **controller.nginxReloadTimeout** | The timeout in milliseconds which the Ingress Controller will wait for a successful NGINX reload after a change or at the initial start. | 60000 | | **controller.hostNetwork** | Enables the Ingress Controller pods to use the host's network namespace. | false | | **controller.dnsPolicy** | DNS policy for the Ingress Controller pods. | ClusterFirst | diff --git a/site/content/installation/installing-nic/installation-with-manifests.md b/site/content/installation/installing-nic/installation-with-manifests.md index f893b42003..178f467718 100644 --- a/site/content/installation/installing-nic/installation-with-manifests.md +++ b/site/content/installation/installing-nic/installation-with-manifests.md @@ -11,6 +11,8 @@ This guide explains how to use Manifests to install F5 NGINX Ingress Controller, ## Before you start +If you are using NGINX Plus, get the NGINX Ingress Controller JWT and [create a license secret]({{< relref "installation/installing-nic/create-license-secret/" >}}). + ### Get the NGINX Controller Image {{< note >}} Always use the latest stable release listed on the [releases page]({{< relref "releases.md" >}}). {{< /note >}} diff --git a/site/content/installation/installing-nic/installation-with-operator.md b/site/content/installation/installing-nic/installation-with-operator.md index 2ccb083a7d..3896ad21ee 100644 --- a/site/content/installation/installing-nic/installation-with-operator.md +++ b/site/content/installation/installing-nic/installation-with-operator.md @@ -11,6 +11,8 @@ This document explains how to use NGINX Ingress Operator to install NGINX Ingres ## Before you start +If you're using NGINX Plus, get the NGINX Ingress Controller JWT and [create a license secret]({{< relref "installation/installing-nic/create-license-secret/" >}}). + {{< note >}} We recommend the most recent stable version of NGINX Ingress Controller, available on the GitHub repository's [releases page]({{< relref "releases.md" >}}). {{< /note >}} 1. Make sure you have access to the NGINX Ingress Controller image: diff --git a/tests/suite/fixtures/fixtures.py b/tests/suite/fixtures/fixtures.py index 45afc17689..c22dc10b64 100644 --- a/tests/suite/fixtures/fixtures.py +++ b/tests/suite/fixtures/fixtures.py @@ -241,10 +241,12 @@ def ingress_controller_prerequisites(cli_arguments, kube_apis, request) -> Ingre ] ) config_map_yaml = f"{DEPLOYMENTS}/common/nginx-config.yaml" + mgmt_config_map_yaml = f"{DEPLOYMENTS}/common/plus-mgmt-configmap.yaml" create_configmap_from_yaml(kube_apis.v1, namespace, config_map_yaml) mgmt_config_map_yaml = f"{DEPLOYMENTS}/common/plus-mgmt-configmap.yaml" with open(config_map_yaml) as f: config_map = yaml.safe_load(f) + create_secret_from_yaml(kube_apis.v1, namespace, f"{TEST_DATA}/common/default-server-secret.yaml") # setup Plus JWT configuration if cli_arguments["ic-type"] == "nginx-plus-ingress": @@ -333,7 +335,7 @@ def cli_arguments(request) -> {}: print(f"Tests will run against the IC of type: {result['ic-type']}") if result["ic-type"] == "nginx-plus-ingress": print(f"Tests will use the Plus JWT") - result["plus-jwt"] = request.config.getoption("--plus-jwt") + result["plus-jwt"] = request.config.getoption("--plus-jwt") result["replicas"] = request.config.getoption("--replicas") print(f"Number of pods spun up will be : {result['replicas']}")