diff --git a/.gitignore b/.gitignore index b9b4cbe17f..2782010765 100644 --- a/.gitignore +++ b/.gitignore @@ -216,6 +216,18 @@ examples/ingress-resources/external-auth-mergeable/external-auth-server-tls-secr examples/custom-resources/external-auth/external-auth-server-tls-secret.yaml examples/custom-resources/external-auth-oauth2/external-auth-server-tls-secret.yaml tests/data/external-auth/backend/external-auth-server-tls-secret.yaml +common-secrets/bundle-server-ca-secret.yaml +common-secrets/bundle-server-tls-secret.yaml +common-secrets/bundle-server-ca-secret-crl.yaml +common-secrets/bundle-client-tls-secret.yaml +examples/shared-examples/waf-bundle-server/bundle-server-ca-secret.yaml +examples/shared-examples/waf-bundle-server/bundle-server-ca-secret-crl.yaml +tests/data/ap-waf-bundle-source/secret/bundle-server-ca-secret.yaml +tests/data/ap-waf-bundle-source/secret/bundle-server-ca-secret-crl.yaml +examples/shared-examples/waf-bundle-server/bundle-client-tls-secret.yaml +tests/data/ap-waf-bundle-source/secret/bundle-client-tls-secret.yaml +examples/shared-examples/waf-bundle-server/bundle-server-tls-secret.yaml +tests/data/ap-waf-bundle-source/secret/bundle-server-tls-secret.yaml # TLS Certificate secrets common-secrets/cafe-passwd-basic-auth-secret.yaml diff --git a/cmd/nginx-ingress/main.go b/cmd/nginx-ingress/main.go index 3a11e1ddfc..3af945ff4e 100644 --- a/cmd/nginx-ingress/main.go +++ b/cmd/nginx-ingress/main.go @@ -343,6 +343,7 @@ func main() { DynamicWeightChangesReload: *enableDynamicWeightChangesReload, InstallationFlags: parsedFlags, ShuttingDown: false, + AppProtectBundlePath: appProtectBundlePath, } lbc := k8s.NewLoadBalancerController(lbcInput) diff --git a/config/crd/bases/k8s.nginx.org_policies.yaml b/config/crd/bases/k8s.nginx.org_policies.yaml index b76983b9d1..d3d20df64d 100644 --- a/config/crd/bases/k8s.nginx.org_policies.yaml +++ b/config/crd/bases/k8s.nginx.org_policies.yaml @@ -828,22 +828,186 @@ spec: properties: apBundle: description: The App Protect WAF policy bundle. Mutually exclusive - with apPolicy. + with apPolicy and apBundleSource. type: string + apBundleSource: + description: |- + ApBundleSource fetches the WAF policy bundle from N1C, NIM, or an HTTPS endpoint. + Mutually exclusive with ApPolicy and ApBundle. + properties: + enablePolling: + description: |- + EnablePolling enables background polling to automatically detect and fetch + updated bundles at the configured PollInterval. When false, the bundle is + fetched once on policy creation or update; subsequent updates require + modifying the Policy resource to trigger a new fetch. + type: boolean + insecureSkipVerify: + description: |- + InsecureSkipVerify disables TLS certificate verification when fetching bundles. + Not recommended for production use. + type: boolean + policyName: + description: PolicyName is the policy name on the management + plane. Required for NIM and N1C; forbidden for HTTPS. + maxLength: 63 + type: string + policyNamespace: + description: PolicyNamespace is the namespace/tenant on the + management plane. Required for N1C only. + maxLength: 63 + type: string + pollInterval: + description: |- + PollInterval is how often to re-fetch the bundle when enablePolling is true. + Minimum 1m. Default 5m. Ignored when enablePolling is false. + type: string + retryAttempts: + description: RetryAttempts is the number of retry attempts + on transient failure. Range 1–10. + maximum: 10 + minimum: 1 + type: integer + secret: + description: |- + Secret is the name of a Kubernetes Secret in the same namespace as the Policy. + For HTTPS: kubernetes.io/tls (tls.crt + tls.key for client mTLS; optional ca.crt for server CA). + For N1C: nginx.com/waf-bundle Secret with a 'token' field containing the API token. + For NIM: nginx.com/waf-bundle Secret with a 'token' field (bearer auth) or 'username'+'password' fields (basic auth). + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + timeout: + description: Timeout is the per-request HTTP timeout. Default + 60s. + type: string + trustedCertSecret: + description: |- + TrustedCertSecret is the name of a Kubernetes Secret with a custom CA certificate + for verifying the remote endpoint TLS certificate. The secret must be in the same + namespace as the Policy, must be of type nginx.org/ca, and must include ca.crt. + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + type: + default: HTTPS + description: Type is the bundle source backend. Defaults to + HTTPS. + enum: + - HTTPS + - NIM + - N1C + type: string + url: + description: URL is the full bundle URL for HTTPS type, or + the API base URL for NIM/N1C. Must use https://. + maxLength: 2083 + minLength: 1 + pattern: ^https:// + type: string + verifyChecksum: + description: VerifyChecksum enables SHA-256 verification of + the downloaded bundle. HTTPS type only. + type: boolean + required: + - enablePolling + - url + type: object apPolicy: description: The App Protect WAF policy of the WAF. Accepts an - optional namespace. Mutually exclusive with apBundle. + optional namespace. Mutually exclusive with apBundle and apBundleSource. type: string enable: description: Enables NGINX App Protect WAF. type: boolean securityLog: - description: SecurityLog defines the security log of a WAF policy. + description: |- + SecurityLog defines the security log of a WAF policy. + Mutual exclusivity of apLogConf, apLogBundle, and apLogBundleSource is enforced by the Go validation layer. properties: apLogBundle: description: The App Protect WAF log bundle resource. Only works with apBundle. type: string + apLogBundleSource: + description: |- + ApLogBundleSource fetches the log profile bundle from N1C, NIM, or an HTTPS endpoint. + Mutually exclusive with ApLogConf and ApLogBundle. Requires apBundleSource on the parent WAF. + properties: + enablePolling: + description: |- + EnablePolling enables background polling to automatically detect and fetch + updated bundles at the configured PollInterval. When false, the bundle is + fetched once on policy creation or update; subsequent updates require + modifying the Policy resource to trigger a new fetch. + type: boolean + insecureSkipVerify: + description: |- + InsecureSkipVerify disables TLS certificate verification when fetching bundles. + Not recommended for production use. + type: boolean + policyName: + description: PolicyName is the policy name on the management + plane. Required for NIM and N1C; forbidden for HTTPS. + maxLength: 63 + type: string + policyNamespace: + description: PolicyNamespace is the namespace/tenant on + the management plane. Required for N1C only. + maxLength: 63 + type: string + pollInterval: + description: |- + PollInterval is how often to re-fetch the bundle when enablePolling is true. + Minimum 1m. Default 5m. Ignored when enablePolling is false. + type: string + retryAttempts: + description: RetryAttempts is the number of retry attempts + on transient failure. Range 1–10. + maximum: 10 + minimum: 1 + type: integer + secret: + description: |- + Secret is the name of a Kubernetes Secret in the same namespace as the Policy. + For HTTPS: kubernetes.io/tls (tls.crt + tls.key for client mTLS; optional ca.crt for server CA). + For N1C: nginx.com/waf-bundle Secret with a 'token' field containing the API token. + For NIM: nginx.com/waf-bundle Secret with a 'token' field (bearer auth) or 'username'+'password' fields (basic auth). + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + timeout: + description: Timeout is the per-request HTTP timeout. + Default 60s. + type: string + trustedCertSecret: + description: |- + TrustedCertSecret is the name of a Kubernetes Secret with a custom CA certificate + for verifying the remote endpoint TLS certificate. The secret must be in the same + namespace as the Policy, must be of type nginx.org/ca, and must include ca.crt. + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + type: + default: HTTPS + description: Type is the bundle source backend. Defaults + to HTTPS. + enum: + - HTTPS + - NIM + - N1C + type: string + url: + description: URL is the full bundle URL for HTTPS type, + or the API base URL for NIM/N1C. Must use https://. + maxLength: 2083 + minLength: 1 + pattern: ^https:// + type: string + verifyChecksum: + description: VerifyChecksum enables SHA-256 verification + of the downloaded bundle. HTTPS type only. + type: boolean + required: + - enablePolling + - url + type: object apLogConf: description: The App Protect WAF log conf resource. Accepts an optional namespace. Only works with apPolicy. @@ -859,12 +1023,95 @@ spec: type: object securityLogs: items: - description: SecurityLog defines the security log of a WAF policy. + description: |- + SecurityLog defines the security log of a WAF policy. + Mutual exclusivity of apLogConf, apLogBundle, and apLogBundleSource is enforced by the Go validation layer. properties: apLogBundle: description: The App Protect WAF log bundle resource. Only works with apBundle. type: string + apLogBundleSource: + description: |- + ApLogBundleSource fetches the log profile bundle from N1C, NIM, or an HTTPS endpoint. + Mutually exclusive with ApLogConf and ApLogBundle. Requires apBundleSource on the parent WAF. + properties: + enablePolling: + description: |- + EnablePolling enables background polling to automatically detect and fetch + updated bundles at the configured PollInterval. When false, the bundle is + fetched once on policy creation or update; subsequent updates require + modifying the Policy resource to trigger a new fetch. + type: boolean + insecureSkipVerify: + description: |- + InsecureSkipVerify disables TLS certificate verification when fetching bundles. + Not recommended for production use. + type: boolean + policyName: + description: PolicyName is the policy name on the management + plane. Required for NIM and N1C; forbidden for HTTPS. + maxLength: 63 + type: string + policyNamespace: + description: PolicyNamespace is the namespace/tenant + on the management plane. Required for N1C only. + maxLength: 63 + type: string + pollInterval: + description: |- + PollInterval is how often to re-fetch the bundle when enablePolling is true. + Minimum 1m. Default 5m. Ignored when enablePolling is false. + type: string + retryAttempts: + description: RetryAttempts is the number of retry attempts + on transient failure. Range 1–10. + maximum: 10 + minimum: 1 + type: integer + secret: + description: |- + Secret is the name of a Kubernetes Secret in the same namespace as the Policy. + For HTTPS: kubernetes.io/tls (tls.crt + tls.key for client mTLS; optional ca.crt for server CA). + For N1C: nginx.com/waf-bundle Secret with a 'token' field containing the API token. + For NIM: nginx.com/waf-bundle Secret with a 'token' field (bearer auth) or 'username'+'password' fields (basic auth). + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + timeout: + description: Timeout is the per-request HTTP timeout. + Default 60s. + type: string + trustedCertSecret: + description: |- + TrustedCertSecret is the name of a Kubernetes Secret with a custom CA certificate + for verifying the remote endpoint TLS certificate. The secret must be in the same + namespace as the Policy, must be of type nginx.org/ca, and must include ca.crt. + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + type: + default: HTTPS + description: Type is the bundle source backend. Defaults + to HTTPS. + enum: + - HTTPS + - NIM + - N1C + type: string + url: + description: URL is the full bundle URL for HTTPS type, + or the API base URL for NIM/N1C. Must use https://. + maxLength: 2083 + minLength: 1 + pattern: ^https:// + type: string + verifyChecksum: + description: VerifyChecksum enables SHA-256 verification + of the downloaded bundle. HTTPS type only. + type: boolean + required: + - enablePolling + - url + type: object apLogConf: description: The App Protect WAF log conf resource. Accepts an optional namespace. Only works with apPolicy. diff --git a/deploy/crds.yaml b/deploy/crds.yaml index f0cbf7ab04..b1208ca522 100644 --- a/deploy/crds.yaml +++ b/deploy/crds.yaml @@ -999,22 +999,186 @@ spec: properties: apBundle: description: The App Protect WAF policy bundle. Mutually exclusive - with apPolicy. + with apPolicy and apBundleSource. type: string + apBundleSource: + description: |- + ApBundleSource fetches the WAF policy bundle from N1C, NIM, or an HTTPS endpoint. + Mutually exclusive with ApPolicy and ApBundle. + properties: + enablePolling: + description: |- + EnablePolling enables background polling to automatically detect and fetch + updated bundles at the configured PollInterval. When false, the bundle is + fetched once on policy creation or update; subsequent updates require + modifying the Policy resource to trigger a new fetch. + type: boolean + insecureSkipVerify: + description: |- + InsecureSkipVerify disables TLS certificate verification when fetching bundles. + Not recommended for production use. + type: boolean + policyName: + description: PolicyName is the policy name on the management + plane. Required for NIM and N1C; forbidden for HTTPS. + maxLength: 63 + type: string + policyNamespace: + description: PolicyNamespace is the namespace/tenant on the + management plane. Required for N1C only. + maxLength: 63 + type: string + pollInterval: + description: |- + PollInterval is how often to re-fetch the bundle when enablePolling is true. + Minimum 1m. Default 5m. Ignored when enablePolling is false. + type: string + retryAttempts: + description: RetryAttempts is the number of retry attempts + on transient failure. Range 1–10. + maximum: 10 + minimum: 1 + type: integer + secret: + description: |- + Secret is the name of a Kubernetes Secret in the same namespace as the Policy. + For HTTPS: kubernetes.io/tls (tls.crt + tls.key for client mTLS; optional ca.crt for server CA). + For N1C: nginx.com/waf-bundle Secret with a 'token' field containing the API token. + For NIM: nginx.com/waf-bundle Secret with a 'token' field (bearer auth) or 'username'+'password' fields (basic auth). + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + timeout: + description: Timeout is the per-request HTTP timeout. Default + 60s. + type: string + trustedCertSecret: + description: |- + TrustedCertSecret is the name of a Kubernetes Secret with a custom CA certificate + for verifying the remote endpoint TLS certificate. The secret must be in the same + namespace as the Policy, must be of type nginx.org/ca, and must include ca.crt. + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + type: + default: HTTPS + description: Type is the bundle source backend. Defaults to + HTTPS. + enum: + - HTTPS + - NIM + - N1C + type: string + url: + description: URL is the full bundle URL for HTTPS type, or + the API base URL for NIM/N1C. Must use https://. + maxLength: 2083 + minLength: 1 + pattern: ^https:// + type: string + verifyChecksum: + description: VerifyChecksum enables SHA-256 verification of + the downloaded bundle. HTTPS type only. + type: boolean + required: + - enablePolling + - url + type: object apPolicy: description: The App Protect WAF policy of the WAF. Accepts an - optional namespace. Mutually exclusive with apBundle. + optional namespace. Mutually exclusive with apBundle and apBundleSource. type: string enable: description: Enables NGINX App Protect WAF. type: boolean securityLog: - description: SecurityLog defines the security log of a WAF policy. + description: |- + SecurityLog defines the security log of a WAF policy. + Mutual exclusivity of apLogConf, apLogBundle, and apLogBundleSource is enforced by the Go validation layer. properties: apLogBundle: description: The App Protect WAF log bundle resource. Only works with apBundle. type: string + apLogBundleSource: + description: |- + ApLogBundleSource fetches the log profile bundle from N1C, NIM, or an HTTPS endpoint. + Mutually exclusive with ApLogConf and ApLogBundle. Requires apBundleSource on the parent WAF. + properties: + enablePolling: + description: |- + EnablePolling enables background polling to automatically detect and fetch + updated bundles at the configured PollInterval. When false, the bundle is + fetched once on policy creation or update; subsequent updates require + modifying the Policy resource to trigger a new fetch. + type: boolean + insecureSkipVerify: + description: |- + InsecureSkipVerify disables TLS certificate verification when fetching bundles. + Not recommended for production use. + type: boolean + policyName: + description: PolicyName is the policy name on the management + plane. Required for NIM and N1C; forbidden for HTTPS. + maxLength: 63 + type: string + policyNamespace: + description: PolicyNamespace is the namespace/tenant on + the management plane. Required for N1C only. + maxLength: 63 + type: string + pollInterval: + description: |- + PollInterval is how often to re-fetch the bundle when enablePolling is true. + Minimum 1m. Default 5m. Ignored when enablePolling is false. + type: string + retryAttempts: + description: RetryAttempts is the number of retry attempts + on transient failure. Range 1–10. + maximum: 10 + minimum: 1 + type: integer + secret: + description: |- + Secret is the name of a Kubernetes Secret in the same namespace as the Policy. + For HTTPS: kubernetes.io/tls (tls.crt + tls.key for client mTLS; optional ca.crt for server CA). + For N1C: nginx.com/waf-bundle Secret with a 'token' field containing the API token. + For NIM: nginx.com/waf-bundle Secret with a 'token' field (bearer auth) or 'username'+'password' fields (basic auth). + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + timeout: + description: Timeout is the per-request HTTP timeout. + Default 60s. + type: string + trustedCertSecret: + description: |- + TrustedCertSecret is the name of a Kubernetes Secret with a custom CA certificate + for verifying the remote endpoint TLS certificate. The secret must be in the same + namespace as the Policy, must be of type nginx.org/ca, and must include ca.crt. + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + type: + default: HTTPS + description: Type is the bundle source backend. Defaults + to HTTPS. + enum: + - HTTPS + - NIM + - N1C + type: string + url: + description: URL is the full bundle URL for HTTPS type, + or the API base URL for NIM/N1C. Must use https://. + maxLength: 2083 + minLength: 1 + pattern: ^https:// + type: string + verifyChecksum: + description: VerifyChecksum enables SHA-256 verification + of the downloaded bundle. HTTPS type only. + type: boolean + required: + - enablePolling + - url + type: object apLogConf: description: The App Protect WAF log conf resource. Accepts an optional namespace. Only works with apPolicy. @@ -1030,12 +1194,95 @@ spec: type: object securityLogs: items: - description: SecurityLog defines the security log of a WAF policy. + description: |- + SecurityLog defines the security log of a WAF policy. + Mutual exclusivity of apLogConf, apLogBundle, and apLogBundleSource is enforced by the Go validation layer. properties: apLogBundle: description: The App Protect WAF log bundle resource. Only works with apBundle. type: string + apLogBundleSource: + description: |- + ApLogBundleSource fetches the log profile bundle from N1C, NIM, or an HTTPS endpoint. + Mutually exclusive with ApLogConf and ApLogBundle. Requires apBundleSource on the parent WAF. + properties: + enablePolling: + description: |- + EnablePolling enables background polling to automatically detect and fetch + updated bundles at the configured PollInterval. When false, the bundle is + fetched once on policy creation or update; subsequent updates require + modifying the Policy resource to trigger a new fetch. + type: boolean + insecureSkipVerify: + description: |- + InsecureSkipVerify disables TLS certificate verification when fetching bundles. + Not recommended for production use. + type: boolean + policyName: + description: PolicyName is the policy name on the management + plane. Required for NIM and N1C; forbidden for HTTPS. + maxLength: 63 + type: string + policyNamespace: + description: PolicyNamespace is the namespace/tenant + on the management plane. Required for N1C only. + maxLength: 63 + type: string + pollInterval: + description: |- + PollInterval is how often to re-fetch the bundle when enablePolling is true. + Minimum 1m. Default 5m. Ignored when enablePolling is false. + type: string + retryAttempts: + description: RetryAttempts is the number of retry attempts + on transient failure. Range 1–10. + maximum: 10 + minimum: 1 + type: integer + secret: + description: |- + Secret is the name of a Kubernetes Secret in the same namespace as the Policy. + For HTTPS: kubernetes.io/tls (tls.crt + tls.key for client mTLS; optional ca.crt for server CA). + For N1C: nginx.com/waf-bundle Secret with a 'token' field containing the API token. + For NIM: nginx.com/waf-bundle Secret with a 'token' field (bearer auth) or 'username'+'password' fields (basic auth). + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + timeout: + description: Timeout is the per-request HTTP timeout. + Default 60s. + type: string + trustedCertSecret: + description: |- + TrustedCertSecret is the name of a Kubernetes Secret with a custom CA certificate + for verifying the remote endpoint TLS certificate. The secret must be in the same + namespace as the Policy, must be of type nginx.org/ca, and must include ca.crt. + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + type: + default: HTTPS + description: Type is the bundle source backend. Defaults + to HTTPS. + enum: + - HTTPS + - NIM + - N1C + type: string + url: + description: URL is the full bundle URL for HTTPS type, + or the API base URL for NIM/N1C. Must use https://. + maxLength: 2083 + minLength: 1 + pattern: ^https:// + type: string + verifyChecksum: + description: VerifyChecksum enables SHA-256 verification + of the downloaded bundle. HTTPS type only. + type: boolean + required: + - enablePolling + - url + type: object apLogConf: description: The App Protect WAF log conf resource. Accepts an optional namespace. Only works with apPolicy. diff --git a/docs/crd/k8s.nginx.org_policies.md b/docs/crd/k8s.nginx.org_policies.md index efba62cf29..d5b85fc0ef 100644 --- a/docs/crd/k8s.nginx.org_policies.md +++ b/docs/crd/k8s.nginx.org_policies.md @@ -138,16 +138,55 @@ The `.spec` object supports the following fields: | `rateLimit.scale` | `boolean` | Enables a constant rate-limit by dividing the configured rate by the number of nginx-ingress pods currently serving traffic. This adjustment ensures that the rate-limit remains consistent, even as the number of nginx-pods fluctuates due to autoscaling. This will not work properly if requests from a client are not evenly distributed across all ingress pods (Such as with sticky sessions, long lived TCP Connections with many requests, and so forth). In such cases using zone-sync instead would give better results. Enabling zone-sync will suppress this setting. | | `rateLimit.zoneSize` | `string` | Size of the shared memory zone. Only positive values are allowed. Allowed suffixes are k or m, if none are present k is assumed. | | `waf` | `object` | The WAF policy configures WAF and log configuration policies for NGINX AppProtect | -| `waf.apBundle` | `string` | The App Protect WAF policy bundle. Mutually exclusive with apPolicy. | -| `waf.apPolicy` | `string` | The App Protect WAF policy of the WAF. Accepts an optional namespace. Mutually exclusive with apBundle. | +| `waf.apBundle` | `string` | The App Protect WAF policy bundle. Mutually exclusive with apPolicy and apBundleSource. | +| `waf.apBundleSource` | `object` | ApBundleSource fetches the WAF policy bundle from N1C, NIM, or an HTTPS endpoint. Mutually exclusive with ApPolicy and ApBundle. | +| `waf.apBundleSource.enablePolling` | `boolean` | EnablePolling enables background polling to automatically detect and fetch updated bundles at the configured PollInterval. When false, the bundle is fetched once on policy creation or update; subsequent updates require modifying the Policy resource to trigger a new fetch. | +| `waf.apBundleSource.insecureSkipVerify` | `boolean` | InsecureSkipVerify disables TLS certificate verification when fetching bundles. Not recommended for production use. | +| `waf.apBundleSource.policyName` | `string` | PolicyName is the policy name on the management plane. Required for NIM and N1C; forbidden for HTTPS. | +| `waf.apBundleSource.policyNamespace` | `string` | PolicyNamespace is the namespace/tenant on the management plane. Required for N1C only. | +| `waf.apBundleSource.pollInterval` | `string` | PollInterval is how often to re-fetch the bundle when enablePolling is true. Minimum 1m. Default 5m. Ignored when enablePolling is false. | +| `waf.apBundleSource.retryAttempts` | `integer` | RetryAttempts is the number of retry attempts on transient failure. Range 1–10. | +| `waf.apBundleSource.secret` | `string` | Secret is the name of a Kubernetes Secret in the same namespace as the Policy. For HTTPS: kubernetes.io/tls (tls.crt + tls.key for client mTLS; optional ca.crt for server CA). For N1C: nginx.com/waf-bundle Secret with a 'token' field containing the API token. For NIM: nginx.com/waf-bundle Secret with a 'token' field (bearer auth) or 'username'+'password' fields (basic auth). | +| `waf.apBundleSource.timeout` | `string` | Timeout is the per-request HTTP timeout. Default 60s. | +| `waf.apBundleSource.trustedCertSecret` | `string` | TrustedCertSecret is the name of a Kubernetes Secret with a custom CA certificate for verifying the remote endpoint TLS certificate. The secret must be in the same namespace as the Policy, must be of type nginx.org/ca, and must include ca.crt. | +| `waf.apBundleSource.type` | `string` | Type is the bundle source backend. Defaults to HTTPS. Allowed values: `"HTTPS"`, `"NIM"`, `"N1C"`. | +| `waf.apBundleSource.url` | `string` | URL is the full bundle URL for HTTPS type, or the API base URL for NIM/N1C. Must use https://. | +| `waf.apBundleSource.verifyChecksum` | `boolean` | VerifyChecksum enables SHA-256 verification of the downloaded bundle. HTTPS type only. | +| `waf.apPolicy` | `string` | The App Protect WAF policy of the WAF. Accepts an optional namespace. Mutually exclusive with apBundle and apBundleSource. | | `waf.enable` | `boolean` | Enables NGINX App Protect WAF. | -| `waf.securityLog` | `object` | SecurityLog defines the security log of a WAF policy. | +| `waf.securityLog` | `object` | SecurityLog defines the security log of a WAF policy. Mutual exclusivity of apLogConf, apLogBundle, and apLogBundleSource is enforced by the Go validation layer. | | `waf.securityLog.apLogBundle` | `string` | The App Protect WAF log bundle resource. Only works with apBundle. | +| `waf.securityLog.apLogBundleSource` | `object` | ApLogBundleSource fetches the log profile bundle from N1C, NIM, or an HTTPS endpoint. Mutually exclusive with ApLogConf and ApLogBundle. Requires apBundleSource on the parent WAF. | +| `waf.securityLog.apLogBundleSource.enablePolling` | `boolean` | EnablePolling enables background polling to automatically detect and fetch updated bundles at the configured PollInterval. When false, the bundle is fetched once on policy creation or update; subsequent updates require modifying the Policy resource to trigger a new fetch. | +| `waf.securityLog.apLogBundleSource.insecureSkipVerify` | `boolean` | InsecureSkipVerify disables TLS certificate verification when fetching bundles. Not recommended for production use. | +| `waf.securityLog.apLogBundleSource.policyName` | `string` | PolicyName is the policy name on the management plane. Required for NIM and N1C; forbidden for HTTPS. | +| `waf.securityLog.apLogBundleSource.policyNamespace` | `string` | PolicyNamespace is the namespace/tenant on the management plane. Required for N1C only. | +| `waf.securityLog.apLogBundleSource.pollInterval` | `string` | PollInterval is how often to re-fetch the bundle when enablePolling is true. Minimum 1m. Default 5m. Ignored when enablePolling is false. | +| `waf.securityLog.apLogBundleSource.retryAttempts` | `integer` | RetryAttempts is the number of retry attempts on transient failure. Range 1–10. | +| `waf.securityLog.apLogBundleSource.secret` | `string` | Secret is the name of a Kubernetes Secret in the same namespace as the Policy. For HTTPS: kubernetes.io/tls (tls.crt + tls.key for client mTLS; optional ca.crt for server CA). For N1C: nginx.com/waf-bundle Secret with a 'token' field containing the API token. For NIM: nginx.com/waf-bundle Secret with a 'token' field (bearer auth) or 'username'+'password' fields (basic auth). | +| `waf.securityLog.apLogBundleSource.timeout` | `string` | Timeout is the per-request HTTP timeout. Default 60s. | +| `waf.securityLog.apLogBundleSource.trustedCertSecret` | `string` | TrustedCertSecret is the name of a Kubernetes Secret with a custom CA certificate for verifying the remote endpoint TLS certificate. The secret must be in the same namespace as the Policy, must be of type nginx.org/ca, and must include ca.crt. | +| `waf.securityLog.apLogBundleSource.type` | `string` | Type is the bundle source backend. Defaults to HTTPS. Allowed values: `"HTTPS"`, `"NIM"`, `"N1C"`. | +| `waf.securityLog.apLogBundleSource.url` | `string` | URL is the full bundle URL for HTTPS type, or the API base URL for NIM/N1C. Must use https://. | +| `waf.securityLog.apLogBundleSource.verifyChecksum` | `boolean` | VerifyChecksum enables SHA-256 verification of the downloaded bundle. HTTPS type only. | | `waf.securityLog.apLogConf` | `string` | The App Protect WAF log conf resource. Accepts an optional namespace. Only works with apPolicy. | | `waf.securityLog.enable` | `boolean` | Enables security log. | | `waf.securityLog.logDest` | `string` | The log destination for the security log. Only accepted variables are syslog:server=; localhost; fqdn>:, stderr, . | | `waf.securityLogs` | `array` | List of configuration values. | | `waf.securityLogs[].apLogBundle` | `string` | The App Protect WAF log bundle resource. Only works with apBundle. | +| `waf.securityLogs[].apLogBundleSource` | `object` | ApLogBundleSource fetches the log profile bundle from N1C, NIM, or an HTTPS endpoint. Mutually exclusive with ApLogConf and ApLogBundle. Requires apBundleSource on the parent WAF. | +| `waf.securityLogs[].apLogBundleSource.enablePolling` | `boolean` | EnablePolling enables background polling to automatically detect and fetch updated bundles at the configured PollInterval. When false, the bundle is fetched once on policy creation or update; subsequent updates require modifying the Policy resource to trigger a new fetch. | +| `waf.securityLogs[].apLogBundleSource.insecureSkipVerify` | `boolean` | InsecureSkipVerify disables TLS certificate verification when fetching bundles. Not recommended for production use. | +| `waf.securityLogs[].apLogBundleSource.policyName` | `string` | PolicyName is the policy name on the management plane. Required for NIM and N1C; forbidden for HTTPS. | +| `waf.securityLogs[].apLogBundleSource.policyNamespace` | `string` | PolicyNamespace is the namespace/tenant on the management plane. Required for N1C only. | +| `waf.securityLogs[].apLogBundleSource.pollInterval` | `string` | PollInterval is how often to re-fetch the bundle when enablePolling is true. Minimum 1m. Default 5m. Ignored when enablePolling is false. | +| `waf.securityLogs[].apLogBundleSource.retryAttempts` | `integer` | RetryAttempts is the number of retry attempts on transient failure. Range 1–10. | +| `waf.securityLogs[].apLogBundleSource.secret` | `string` | Secret is the name of a Kubernetes Secret in the same namespace as the Policy. For HTTPS: kubernetes.io/tls (tls.crt + tls.key for client mTLS; optional ca.crt for server CA). For N1C: nginx.com/waf-bundle Secret with a 'token' field containing the API token. For NIM: nginx.com/waf-bundle Secret with a 'token' field (bearer auth) or 'username'+'password' fields (basic auth). | +| `waf.securityLogs[].apLogBundleSource.timeout` | `string` | Timeout is the per-request HTTP timeout. Default 60s. | +| `waf.securityLogs[].apLogBundleSource.trustedCertSecret` | `string` | TrustedCertSecret is the name of a Kubernetes Secret with a custom CA certificate for verifying the remote endpoint TLS certificate. The secret must be in the same namespace as the Policy, must be of type nginx.org/ca, and must include ca.crt. | +| `waf.securityLogs[].apLogBundleSource.type` | `string` | Type is the bundle source backend. Defaults to HTTPS. Allowed values: `"HTTPS"`, `"NIM"`, `"N1C"`. | +| `waf.securityLogs[].apLogBundleSource.url` | `string` | URL is the full bundle URL for HTTPS type, or the API base URL for NIM/N1C. Must use https://. | +| `waf.securityLogs[].apLogBundleSource.verifyChecksum` | `boolean` | VerifyChecksum enables SHA-256 verification of the downloaded bundle. HTTPS type only. | | `waf.securityLogs[].apLogConf` | `string` | The App Protect WAF log conf resource. Accepts an optional namespace. Only works with apPolicy. | | `waf.securityLogs[].enable` | `boolean` | Enables security log. | | `waf.securityLogs[].logDest` | `string` | The log destination for the security log. Only accepted variables are syslog:server=; localhost; fqdn>:, stderr, . | diff --git a/examples/custom-resources/security-monitoring-v5/README.md b/examples/custom-resources/security-monitoring-v5/README.md index 0b2c48ff52..74bf3c073b 100644 --- a/examples/custom-resources/security-monitoring-v5/README.md +++ b/examples/custom-resources/security-monitoring-v5/README.md @@ -2,6 +2,11 @@ This example describes how to deploy NGINX Plus Ingress Controller with [F5 WAF for NGINX v5](https://docs.nginx.com/waf/) and [NGINX Agent](https://docs.nginx.com/nginx-agent/overview/) to integrate with NGINX Security Monitoring. It deploys a simple web application and configures WAF protection using compiled policy and log bundles, forwarding security logs to the Security Monitoring dashboard via syslog. +WAF policy bundles can be sourced in two ways: + +- **Filesystem bundles** (`apBundle`) — manually compile and copy `.tgz` bundles to the pod volume. See Steps 2–4 below. +- **Remote bundle sources** (`apBundleSource`) — NIC automatically fetches bundles from NGINX One Console (N1C), NGINX Instance Manager (NIM), or any HTTPS endpoint. See [WAF with Management Plane Sources](../waf-management-plane/) or [WAF with HTTPS Bundle Sources](../waf-https-bundles/) for details, or use `waf-n1c.yaml` / `waf-nim.yaml` in Step 4 below. + This example works with both: - **NGINX Instance Manager** (Agent 2.*) - See the [Security Monitoring tutorial](https://docs.nginx.com/nginx-ingress-controller/tutorials/security-monitoring/) for agent configuration. @@ -45,7 +50,9 @@ Create the application deployment and service: kubectl apply -f webapp.yaml ``` -## Step 2 - Create and Deploy the WAF Policy and Log Bundles +## Step 2 - Create and Deploy the WAF Policy and Log Bundles (filesystem bundles only) + +> **Skip this step** if using remote bundle sources (`waf-n1c.yaml`) — NIC fetches bundles automatically. 1. Compile your WAF policy and log configuration into bundles (`.tgz` files) using the `waf-compiler` image. See [Compile WAF Policy from JSON to Bundle](https://docs.nginx.com/nginx-ingress-controller/install/waf-helm/#compile-waf-policy-from-json-to-bundle) for compilation steps. @@ -70,21 +77,54 @@ If you are using Agent (3.*) (NGINX One Console), skip this step. NGINX Agent 3. ## Step 4 - Deploy the WAF Policy -Create the WAF policy referencing the compiled bundles. Choose the file that matches your agent version: +Create the WAF policy referencing the compiled bundles. Choose the file that matches your setup: -**Agent 2.* (NGINX Instance Manager)** — logs sent to the syslog service: +**Agent 2.* (NGINX Instance Manager)** — filesystem bundles, logs sent to the syslog service: ```console kubectl apply -f waf.yaml ``` -**Agent 3.* (NGINX One Console)** — logs sent directly to the local NGINX Agent listener: +**Agent 2.* (NGINX Instance Manager) with remote bundle pulling** — bundles fetched from NIM via `apBundleSource`, no manual file copy needed: + +```console +kubectl create secret generic nim-credentials \ + --type=nginx.com/waf-bundle \ + --from-literal=token= +kubectl apply -f waf-nim.yaml +``` + +Edit `waf-nim.yaml` and replace ``, ``, and `` with your NGINX Instance Manager values. + +> When using `waf-nim.yaml`, skip Step 2 (manual bundle compilation and copy) — NIC fetches bundles directly from NGINX Instance Manager. + +**Agent 3.* (NGINX One Console)** — filesystem bundles, logs sent to the local NGINX Agent listener: ```console kubectl apply -f waf-agent-v3.yaml ``` -Note the log bundle referenced in the `apLogBundle` field must be compiled from a log profile that matches the format required by NGINX Security Monitoring. +**Agent 3.* (NGINX One Console) with remote bundle pulling** — bundles fetched from N1C via `apBundleSource`, no manual file copy needed: + +```console +kubectl create secret generic n1c-credentials \ + --type=nginx.com/waf-bundle \ + --from-literal=token= +kubectl apply -f waf-n1c.yaml +``` + +Edit `waf-n1c.yaml` and replace `` and `` with your NGINX One Console values. The API token is generated from the [F5 Distributed Cloud Console](https://console.ves.volterra.io) under **Account Settings > Credentials > Add Credentials > API Token**. See [Managing User Credentials](https://docs.cloud.f5.com/docs/how-to/user-mgmt/credentials) for details. + +> When using `waf-n1c.yaml`, skip Step 2 (manual bundle compilation and copy) — NIC fetches bundles directly from NGINX One Console. + +Verify the policy status: + +```console +kubectl describe policy waf-policy +``` + +The policy should show `State: Valid`. If the bundle source is unreachable, the status will be +`Warning` with reason `BundleFetchFailed`. NIC retries on the next `pollInterval`. ## Step 5 - Configure Load Balancing diff --git a/examples/custom-resources/security-monitoring-v5/waf-n1c.yaml b/examples/custom-resources/security-monitoring-v5/waf-n1c.yaml new file mode 100644 index 0000000000..c588038a4b --- /dev/null +++ b/examples/custom-resources/security-monitoring-v5/waf-n1c.yaml @@ -0,0 +1,26 @@ +apiVersion: k8s.nginx.org/v1 +kind: Policy +metadata: + name: waf-policy +spec: + waf: + enable: true + apBundleSource: + type: N1C + url: "https://.console.ves.volterra.io" + policyName: "" + policyNamespace: "default" + secret: "n1c-credentials" + enablePolling: true + pollInterval: "5m" + securityLogs: + - enable: true + apLogBundleSource: + type: N1C + url: "https://.console.ves.volterra.io" + policyName: "secops_dashboard" + policyNamespace: "default" + secret: "n1c-credentials" + enablePolling: true + pollInterval: "5m" + logDest: "syslog:server=127.0.0.1:1514" diff --git a/examples/custom-resources/security-monitoring-v5/waf-nim.yaml b/examples/custom-resources/security-monitoring-v5/waf-nim.yaml new file mode 100644 index 0000000000..99f3b8f031 --- /dev/null +++ b/examples/custom-resources/security-monitoring-v5/waf-nim.yaml @@ -0,0 +1,24 @@ +apiVersion: k8s.nginx.org/v1 +kind: Policy +metadata: + name: waf-policy +spec: + waf: + enable: true + apBundleSource: + type: NIM + url: "https://" + policyName: "" + secret: "nim-credentials" + enablePolling: true + pollInterval: "5m" + securityLogs: + - enable: true + apLogBundleSource: + type: NIM + url: "https://" + policyName: "" + secret: "nim-credentials" + enablePolling: true + pollInterval: "5m" + logDest: "syslog:server=syslog-svc.default:514" diff --git a/examples/custom-resources/waf-https-bundles/README.md b/examples/custom-resources/waf-https-bundles/README.md new file mode 100644 index 0000000000..317322cbc9 --- /dev/null +++ b/examples/custom-resources/waf-https-bundles/README.md @@ -0,0 +1,149 @@ +# WAF with HTTPS Bundle Source + +In this example we deploy the NGINX Plus Ingress Controller with [F5 WAF for NGINX v5](https://docs.nginx.com/waf/) and configure WAF protection using pre-compiled policy bundles fetched from an HTTPS endpoint. + +The HTTPS source type works with any server that can serve a compiled `.tgz` bundle over HTTPS — for example, an artifact repository, a CI/CD pipeline output, or any static file server. In this example, we use a self-contained [bundle server](../../shared-examples/waf-bundle-server/) that compiles and serves bundles using the `waf-compiler` image from the F5 private registry. + +For sourcing bundles from NGINX Instance Manager or NGINX One Console instead, see [waf-management-plane](../waf-management-plane/). + +## Prerequisites + +1. Follow the installation [instructions](https://docs.nginx.com/nginx-ingress-controller/installation) to deploy the + Ingress Controller with F5 WAF for NGINX v5. + +1. An `imagePullSecret` named `regcred` in the `default` namespace with access to + `private-registry.nginx.com` (required by the bundle server's `waf-compiler` init containers). This is the same secret used + for NIC. See [Download NGINX Ingress Controller from the F5 Registry](https://docs.nginx.com/nginx-ingress-controller/install/images/registry-download/). + +1. Save the public IP address of the Ingress Controller into a shell variable: + + ```console + IC_IP=XXX.YYY.ZZZ.III + ``` + +1. Save the HTTP port of the Ingress Controller into a shell variable: + + ```console + IC_HTTP_PORT= + ``` + +## Step 1. Deploy a Web Application + +Create the application deployment and service: + +```console +kubectl apply -f webapp.yaml +``` + +## Step 2 - Generate the TLS Secrets + +Run `make secrets` from the repository root to generate the TLS certificates used by the bundle server and NIC: + +```console +make secrets +``` + +This creates: + +- `bundle-server-tls` — server certificate for the bundle server HTTPS endpoint +- `bundle-server-ca` — CA certificate for server verification +- `bundle-client-tls` — client certificate for mTLS authentication + +Apply the generated secrets: + +```console +kubectl apply -f ../../shared-examples/waf-bundle-server/bundle-server-tls-secret.yaml +kubectl apply -f ../../shared-examples/waf-bundle-server/bundle-server-ca-secret.yaml +kubectl apply -f ../../shared-examples/waf-bundle-server/bundle-client-tls-secret.yaml +``` + +## Step 3 - Deploy the Bundle Server + +The bundle server compiles WAF policy and log profile JSON definitions into `.tgz` bundles at startup using `waf-compiler` init containers, then serves them over HTTPS with mTLS: + +```console +kubectl apply -f ../../shared-examples/waf-bundle-server/deployment.yaml +``` + +Wait for the bundle server pod to be ready (init containers compile the bundles first): + +```console +kubectl wait --for=condition=ready pod -l app=bundle-server --timeout=120s +``` + +The compiled bundles are available at: + +- `https://bundle-server.default.svc.cluster.local/bundles/attack-signatures-blocking.tgz` +- `https://bundle-server.default.svc.cluster.local/bundles/log-default.tgz` + +See the [bundle server README](../../shared-examples/waf-bundle-server/) for details on customizing policies. + +## Step 4 - Deploy the WAF Policy + +Create the WAF policy that fetches bundles from the HTTPS bundle server: + +```console +kubectl apply -f waf-https.yaml +``` + +Verify the policy status: + +```console +kubectl describe policy waf-policy +``` + +The policy should show `State: Valid` once the bundle has been fetched and applied. If the bundle server is unreachable, the status will be `Warning` with reason `BundleFetchFailed`. NIC retries on the next poll interval. + +## Step 5 - Configure Load Balancing + +Create the VirtualServer resource: + +```console +kubectl apply -f virtual-server.yaml +``` + +Note that the VirtualServer references the policy `waf-policy` created in Step 4. + +## Step 6 - Test the Application + +1. Send a valid request to the application: + + ```console + curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP http://webapp.example.com:$IC_HTTP_PORT/ + ``` + + ```text + Server address: 10.12.0.18:80 + Server name: webapp-7586895968-r26zn + ... + ``` + +1. Send a request with a suspicious URL: + + ```console + curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP "http://webapp.example.com:$IC_HTTP_PORT/", + headers={"host": virtual_server_setup.vs_host}, + ) + assert "The requested URL was rejected" not in response.text + + restore_default_vs(kube_apis, virtual_server_setup) + delete_policy(kube_apis.custom_objects, pol_name, test_namespace) + + def test_bundle_source_recovery( + self, + kube_apis, + ingress_controller_prerequisites, + crd_ingress_controller_with_waf_v5, + virtual_server_setup, + test_namespace, + bundle_server, + ): + # Step 1: Create policy with unreachable URL. + pol_name = create_waf_bundle_source_policy( + kube_apis.custom_objects, + test_namespace, + POLICY_NAME, + INVALID_BUNDLE_URL, + insecure_skip_verify=True, + ) + + patch_virtual_server_from_yaml( + kube_apis.custom_objects, + virtual_server_setup.vs_name, + WAF_SPEC_VS, + virtual_server_setup.namespace, + ) + wait_before_test(5) + + # Confirm Warning status. + policy_info = read_custom_resource( + kube_apis.custom_objects, + test_namespace, + "policies", + POLICY_NAME, + ) + assert policy_info.get("status", {}).get("state") == "Warning" + + # Step 2: Patch policy to a valid bundle URL. + patch_waf_bundle_source_policy( + kube_apis.custom_objects, + test_namespace, + POLICY_NAME, + bundle_server.insecure_url, + insecure_skip_verify=True, + ) + + # Step 3: Wait for WAF to become active and verify blocking. + response = send_malicious_request_with_retry( + virtual_server_setup.backend_1_url, + virtual_server_setup.vs_host, + retries=25, + ) + + restore_default_vs(kube_apis, virtual_server_setup) + delete_policy(kube_apis.custom_objects, pol_name, test_namespace) + + assert_waf_blocked(response) diff --git a/tests/suite/test_app_protect_waf_policies_ing.py b/tests/suite/test_app_protect_waf_policies_ing.py index 01191aa841..81626a754f 100644 --- a/tests/suite/test_app_protect_waf_policies_ing.py +++ b/tests/suite/test_app_protect_waf_policies_ing.py @@ -1,5 +1,4 @@ import pytest -import requests from settings import TEST_DATA from suite.fixtures.fixtures import PublicEndpoint from suite.utils.ap_resources_utils import ( @@ -10,6 +9,7 @@ delete_ap_logconf, delete_ap_policy, delete_ap_usersig, + send_malicious_request_with_retry, ) from suite.utils.policy_resources_utils import delete_policy from suite.utils.resources_utils import ( @@ -42,16 +42,6 @@ def assert_waf_rejected(response): assert "The requested URL was rejected. Please consult with your administrator." in response.text -def send_malicious_request_with_retry(url, host): - response = requests.get(url + "", headers={"host": host}) - retries = 0 - while retries < 5 and "Request Rejected" not in response.text: - wait_before_test(1) - response = requests.get(url + "", headers={"host": host}) - retries += 1 - return response - - def create_ingress_setup(kube_apis, ingress_controller_endpoint, test_namespace, ingress_src): create_example_app(kube_apis, "simple", test_namespace) wait_until_all_pods_are_ready(kube_apis.v1, test_namespace) @@ -148,7 +138,7 @@ def test_waf_policy_v4_on_ingress( wait_before_test(120) request_url = f"http://{ingress_setup.public_endpoint.public_ip}:{ingress_setup.public_endpoint.port}/backend1" ensure_response_from_backend(request_url, ingress_setup.ingress_host, check404=True) - response = send_malicious_request_with_retry(request_url, ingress_setup.ingress_host) + response = send_malicious_request_with_retry(request_url, ingress_setup.ingress_host, retries=5, wait_seconds=1) delete_policy(kube_apis.custom_objects, "waf-policy", test_namespace) assert_waf_rejected(response) @@ -197,7 +187,9 @@ def test_waf_policy_v4_on_mergeable_ingress( f"{mergeable_ingress_setup.public_endpoint.port}/backend1" ) ensure_response_from_backend(request_url, mergeable_ingress_setup.ingress_host, check404=True) - response = send_malicious_request_with_retry(request_url, mergeable_ingress_setup.ingress_host) + response = send_malicious_request_with_retry( + request_url, mergeable_ingress_setup.ingress_host, retries=5, wait_seconds=1 + ) delete_policy(kube_apis.custom_objects, "waf-policy", test_namespace) assert_waf_rejected(response) diff --git a/tests/suite/test_app_protect_wafv5_policies_ing.py b/tests/suite/test_app_protect_wafv5_policies_ing.py index 07cf9f9201..6d7c8b0f0e 100644 --- a/tests/suite/test_app_protect_wafv5_policies_ing.py +++ b/tests/suite/test_app_protect_wafv5_policies_ing.py @@ -1,7 +1,7 @@ import pytest -import requests from settings import TEST_DATA from suite.fixtures.fixtures import PublicEndpoint +from suite.utils.ap_resources_utils import send_malicious_request_with_retry from suite.utils.policy_resources_utils import create_policy_from_yaml, delete_policy from suite.utils.resources_utils import ( create_example_app, @@ -32,16 +32,6 @@ def assert_waf_rejected(response): assert "The requested URL was rejected. Please consult with your administrator." in response.text -def send_malicious_request_with_retry(url, host): - response = requests.get(url + "", headers={"host": host}) - retries = 0 - while retries < 5 and "Request Rejected" not in response.text: - wait_before_test(1) - response = requests.get(url + "", headers={"host": host}) - retries += 1 - return response - - def create_ingress_setup(kube_apis, ingress_controller_endpoint, test_namespace, ingress_src): create_example_app(kube_apis, "simple", test_namespace) wait_until_all_pods_are_ready(kube_apis.v1, test_namespace) @@ -106,7 +96,7 @@ def test_waf_policy_v5_on_ingress( wait_before_test() request_url = f"http://{ingress_setup.public_endpoint.public_ip}:{ingress_setup.public_endpoint.port}/backend1" - response = send_malicious_request_with_retry(request_url, ingress_setup.ingress_host) + response = send_malicious_request_with_retry(request_url, ingress_setup.ingress_host, retries=5, wait_seconds=1) delete_policy(kube_apis.custom_objects, "waf-policy", test_namespace) assert_waf_rejected(response) @@ -144,7 +134,9 @@ def test_waf_policy_v5_on_mergeable_ingress( f"http://{mergeable_ingress_setup.public_endpoint.public_ip}:" f"{mergeable_ingress_setup.public_endpoint.port}/backend1" ) - response = send_malicious_request_with_retry(request_url, mergeable_ingress_setup.ingress_host) + response = send_malicious_request_with_retry( + request_url, mergeable_ingress_setup.ingress_host, retries=5, wait_seconds=1 + ) delete_policy(kube_apis.custom_objects, "waf-policy", test_namespace) assert_waf_rejected(response) diff --git a/tests/suite/utils/ap_resources_utils.py b/tests/suite/utils/ap_resources_utils.py index 1107480bdc..bc186b4300 100644 --- a/tests/suite/utils/ap_resources_utils.py +++ b/tests/suite/utils/ap_resources_utils.py @@ -3,10 +3,11 @@ import logging import time +import requests import yaml from kubernetes.client import CustomObjectsApi from kubernetes.client.rest import ApiException -from suite.utils.resources_utils import ensure_item_removal +from suite.utils.resources_utils import ensure_item_removal, wait_before_test def read_ap_custom_resource(custom_objects: CustomObjectsApi, namespace, plural, name) -> object: @@ -67,7 +68,10 @@ def create_ap_waf_policy_from_yaml( print(f"Policy created: {dep}") return dep["metadata"]["name"] except ApiException: - logging.error(f"Exception occurred while creating Policy: {dep['metadata']['name']}", exc_info=True) + logging.error( + f"Exception occurred while creating Policy: {dep['metadata']['name']}", + exc_info=True, + ) raise @@ -105,11 +109,18 @@ def create_ap_multilog_waf_policy_from_yaml( try: for i in range(len(aplogconfs)): seclogs.append( - {"enable": True, "apLogConf": f"{ap_namespace}/{aplogconfs[i]}", "logDest": f"{logdests[i]}"} + { + "enable": True, + "apLogConf": f"{ap_namespace}/{aplogconfs[i]}", + "logDest": f"{logdests[i]}", + } ) dep["spec"]["waf"]["securityLogs"] = seclogs except KeyError: - logging.error(f"Exception occurred while creating Policy: {dep['metadata']['name']}", exc_info=True) + logging.error( + f"Exception occurred while creating Policy: {dep['metadata']['name']}", + exc_info=True, + ) raise del dep["spec"]["waf"]["securityLog"] @@ -117,7 +128,10 @@ def create_ap_multilog_waf_policy_from_yaml( print(f"Policy created: {dep}") return dep["metadata"]["name"] except ApiException: - logging.error(f"Exception occurred while creating Policy: {dep['metadata']['name']}", exc_info=True) + logging.error( + f"Exception occurred while creating Policy: {dep['metadata']['name']}", + exc_info=True, + ) raise @@ -250,3 +264,20 @@ def delete_ap_policy(custom_objects: CustomObjectsApi, name, namespace) -> None: ) time.sleep(3) print(f"AP policy was removed with name: {name}") + + +def send_malicious_request_with_retry(url, host, retries=20, wait_seconds=3): + """Send a request with an embedded XSS payload, retrying until WAF blocks it.""" + response = requests.get(url + "", headers={"host": host}) + count = 0 + while count < retries and "Request Rejected" not in response.text: + wait_before_test(wait_seconds) + response = requests.get(url + "", headers={"host": host}) + count += 1 + return response + + +def assert_waf_blocked(response): + """Assert that the response was rejected by App Protect WAF.""" + assert response.status_code == 200 + assert "The requested URL was rejected. Please consult with your administrator." in response.text diff --git a/tests/suite/utils/bundle_source_utils.py b/tests/suite/utils/bundle_source_utils.py new file mode 100644 index 0000000000..0f92bd86bd --- /dev/null +++ b/tests/suite/utils/bundle_source_utils.py @@ -0,0 +1,268 @@ +"""Utilities for WAF bundle source integration tests. + +Provides helpers to deploy an in-cluster HTTPS bundle server using +pre-generated TLS secrets (from hack/secrets-gen) and to create WAF +Policy CRDs with apBundleSource. +""" + +import logging +import os +import subprocess + +from kubernetes.client import CoreV1Api, CustomObjectsApi +from kubernetes.client.rest import ApiException +from settings import TEST_DATA +from suite.utils.resources_utils import ( + create_items_from_yaml, + create_secret_from_yaml, + delete_items_from_yaml, + delete_secret, + wait_before_test, + wait_until_all_pods_are_ready, +) + +BUNDLE_SERVER_YAML = f"{TEST_DATA}/ap-waf-bundle-source/bundle-server.yaml" +WAF_BUNDLE_PATH = f"{TEST_DATA}/ap-waf-v5/wafv5.tgz" + +# Pre-generated by make secrets (hack/secrets-gen), symlinked into tests/data/. +SECRET_DIR = f"{TEST_DATA}/ap-waf-bundle-source/secret" +SERVER_TLS_SECRET_YAML = f"{SECRET_DIR}/bundle-server-tls-secret.yaml" +CLIENT_TLS_SECRET_YAML = f"{SECRET_DIR}/bundle-client-tls-secret.yaml" +CA_SECRET_YAML = f"{SECRET_DIR}/bundle-server-ca-secret.yaml" + +SECRET_NAME_SERVER_TLS = "bundle-server-tls" +SECRET_NAME_CLIENT_TLS = "bundle-client-tls" +SECRET_NAME_CA = "bundle-server-ca" + +BUNDLE_SERVER_SECRETS = [SECRET_NAME_SERVER_TLS, SECRET_NAME_CLIENT_TLS, SECRET_NAME_CA] + + +class BundleServerSetup: + """Holds connection info for the deployed HTTPS bundle server.""" + + def __init__(self, insecure_url: str, mtls_url: str, client_secret: str, ca_secret: str): + self.insecure_url = insecure_url + self.mtls_url = mtls_url + self.client_secret = client_secret + self.ca_secret = ca_secret + + +def create_bundle_server_secrets(v1: CoreV1Api, namespace: str) -> None: + """Create Kubernetes TLS secrets from pre-generated YAML files. + + The YAML files are produced by ``make secrets`` (hack/secrets-gen) + and symlinked into tests/data/ap-waf-bundle-source/secret/. + + :param v1: CoreV1Api + :param namespace: target namespace + """ + for yaml_path in [SERVER_TLS_SECRET_YAML, CLIENT_TLS_SECRET_YAML, CA_SECRET_YAML]: + create_secret_from_yaml(v1, namespace, yaml_path) + + +def deploy_bundle_server(kube_apis, namespace: str) -> None: + """Deploy the HTTPS bundle server and copy the WAF bundle into it. + + Applies bundle-server.yaml (ConfigMap + Deployment + Services), waits + for the pod to become ready, then streams wafv5.tgz into the pod. + + :param kube_apis: KubeApis fixture object + :param namespace: target namespace + """ + print("Deploy bundle server") + create_items_from_yaml(kube_apis, BUNDLE_SERVER_YAML, namespace) + wait_until_all_pods_are_ready(kube_apis.v1, namespace) + _copy_bundle_to_pod(kube_apis.v1, namespace) + + +def _copy_bundle_to_pod(v1: CoreV1Api, namespace: str) -> None: + """Copy the pre-compiled WAF bundle into the bundle-server pod. + + Uses kubectl cp which handles buffering and EOF signaling reliably, + unlike the kubernetes python client's websocket stream which can + truncate large files. + """ + if not os.path.exists(WAF_BUNDLE_PATH): + raise FileNotFoundError(f"WAF bundle not found: {WAF_BUNDLE_PATH}") + + pod_name = _get_bundle_server_pod(v1, namespace) + dest_path = "/www/bundles/wafv5.tgz" + print(f"Copy WAF bundle to bundle-server pod {pod_name}") + + result = subprocess.run( + ["kubectl", "cp", WAF_BUNDLE_PATH, f"{namespace}/{pod_name}:{dest_path}", "-c", "nginx"], + capture_output=True, + text=True, + timeout=60, + ) + if result.returncode != 0: + raise RuntimeError(f"kubectl cp failed: {result.stderr}") + print("WAF bundle copied to bundle-server pod") + + +def _get_bundle_server_pod(v1: CoreV1Api, namespace: str) -> str: + """Return the name of the bundle-server pod.""" + pods = v1.list_namespaced_pod(namespace, label_selector="app=bundle-server") + if not pods.items: + raise RuntimeError(f"No bundle-server pod found in namespace {namespace}") + return pods.items[0].metadata.name + + +def teardown_bundle_server(kube_apis, namespace: str) -> None: + """Remove the bundle server deployment and all associated secrets. + + :param kube_apis: KubeApis fixture object + :param namespace: target namespace + """ + print("Teardown bundle server") + try: + delete_items_from_yaml(kube_apis, BUNDLE_SERVER_YAML, namespace) + except Exception as ex: + logging.warning(f"Error deleting bundle server resources: {ex}") + + for secret_name in BUNDLE_SERVER_SECRETS: + try: + delete_secret(kube_apis.v1, secret_name, namespace) + except ApiException as ex: + if ex.status != 404: + logging.warning(f"Error deleting secret {secret_name}: {ex}") + + +def create_waf_bundle_source_policy( + custom_objects: CustomObjectsApi, + namespace: str, + name: str, + url: str, + *, + enable_polling: bool = False, + insecure_skip_verify: bool = False, + secret: str = None, + trusted_cert_secret: str = None, +) -> str: + """Create a WAF Policy CRD with apBundleSource (built programmatically). + + :param custom_objects: CustomObjectsApi + :param namespace: target namespace + :param name: policy name + :param url: bundle URL (must be https://) + :param enable_polling: whether to enable background polling + :param insecure_skip_verify: skip TLS certificate verification + :param secret: name of kubernetes.io/tls secret for client mTLS + :param trusted_cert_secret: name of nginx.org/ca secret for server CA verification + :return: policy name + """ + bundle_source = { + "url": url, + "enablePolling": enable_polling, + } + if insecure_skip_verify: + bundle_source["insecureSkipVerify"] = True + if secret: + bundle_source["secret"] = secret + if trusted_cert_secret: + bundle_source["trustedCertSecret"] = trusted_cert_secret + + policy = { + "apiVersion": "k8s.nginx.org/v1", + "kind": "Policy", + "metadata": {"name": name}, + "spec": { + "waf": { + "enable": True, + "apBundleSource": bundle_source, + } + }, + } + + print(f"Create WAF bundle source policy: {name}") + try: + custom_objects.create_namespaced_custom_object( + "k8s.nginx.org", + "v1", + namespace, + "policies", + policy, + ) + print(f"WAF bundle source policy created: {name}") + except ApiException: + logging.exception(f"Failed to create WAF bundle source policy: {name}") + raise + + return name + + +def patch_waf_bundle_source_policy( + custom_objects: CustomObjectsApi, + namespace: str, + name: str, + url: str, + *, + enable_polling: bool = False, + insecure_skip_verify: bool = False, + secret: str = None, + trusted_cert_secret: str = None, +) -> None: + """Patch an existing WAF Policy's apBundleSource URL and settings. + + Used for recovery tests where we switch from an invalid to a valid URL. + """ + bundle_source = { + "url": url, + "enablePolling": enable_polling, + } + if insecure_skip_verify: + bundle_source["insecureSkipVerify"] = True + if secret: + bundle_source["secret"] = secret + if trusted_cert_secret: + bundle_source["trustedCertSecret"] = trusted_cert_secret + + patch_body = { + "spec": { + "waf": { + "enable": True, + "apBundleSource": bundle_source, + } + }, + } + + print(f"Patch WAF bundle source policy: {name} with url={url}") + custom_objects.patch_namespaced_custom_object( + "k8s.nginx.org", + "v1", + namespace, + "policies", + name, + patch_body, + ) + print(f"WAF bundle source policy patched: {name}") + + +def bundle_server_insecure_url(namespace: str) -> str: + """Return the TLS-only (no client auth) bundle URL for the given namespace.""" + return f"https://bundle-server.{namespace}.svc.cluster.local:8443/bundles/wafv5.tgz" + + +def bundle_server_mtls_url(namespace: str) -> str: + """Return the mTLS bundle URL for the given namespace.""" + return f"https://bundle-server-mtls.{namespace}.svc.cluster.local/bundles/wafv5.tgz" + + +def setup_bundle_server(kube_apis, namespace: str) -> BundleServerSetup: + """Full setup: create secrets from pre-generated YAML, deploy server, copy bundle. + + :param kube_apis: KubeApis fixture object + :param namespace: target namespace + :return: BundleServerSetup with URLs and secret names + """ + create_bundle_server_secrets(kube_apis.v1, namespace) + deploy_bundle_server(kube_apis, namespace) + # Allow time for service endpoints to fully propagate (especially mTLS on port 443). + wait_before_test(5) + + return BundleServerSetup( + insecure_url=bundle_server_insecure_url(namespace), + mtls_url=bundle_server_mtls_url(namespace), + client_secret=SECRET_NAME_CLIENT_TLS, + ca_secret=SECRET_NAME_CA, + )