nic-add-policy
nginx/kubernetes-ingress/.github/skills/nic-add-policy/SKILL.md
Step-by-step checklist for adding a new Policy CRD type to NIC. Use when implementing a new policy like AccessControl, RateLimit, JWTAuth, ExternalAuth, BasicAuth, IngressMTLS, EgressMTLS, OIDC, WAF, APIKey, Cache, or CORS, or extending the policy system with a new policy type.
Skill5.1k starsChanged 22 days ago
What's in it
- Adding a New Policy Type
- Step 1: Define the CRD type
- Step 2: Regenerate deep copy
- Step 3: Regenerate CRDs
- Step 4: Add validation
- Step 5: Add template structs
- Step 6: Add config generation
- Step 7: Wire into VirtualServer generation
- Step 8: Wire into Ingress generation (if applicable)
- Step 9: Add NGINX template directives
- Step 10: Update snapshot tests
- Step 11: Update the Helm chart (if policy needs CLI flag or ConfigMap entry)
- Step 12: Add controller support
- Step 13: Write integration tests
- Gotchas
- Policy addConfig() Pattern
- NGINX Template Pattern
---
name: nic-add-policy
description: 'Step-by-step checklist for adding a new Policy CRD type to NIC. Use when implementing a new policy like AccessControl, RateLimit, JWTAuth, ExternalAuth, BasicAuth, IngressMTLS, EgressMTLS, OIDC, WAF, APIKey, Cache, or CORS, or extending the policy system with a new policy type.'
---
# Adding a New Policy Type
Follow these steps IN ORDER. Each step depends on the previous.
## Step 1: Define the CRD type
File: `pkg/apis/configuration/v1/types.go`
- Add a new struct (e.g., `type MyPolicy struct { ... }`)
- Add a `*MyPolicy` pointer field to `PolicySpec`
- Use kubebuilder markers for validation
- JSON tags: **kebab-case** for NGINX-proxy fields, **camelCase** for K8s fields
- `*bool`/`*int` = optional/nullable. Plain `bool`/`int` = required or zero-default
- Booleans defaulting to `false` must be non-pointer value types
## Step 2: Regenerate deep copy
Run `make update-codegen` to update `zz_generated.deepcopy.go`.
## Step 3: Regenerate CRDs
Run `make update-crds` to regenerate `config/crd/bases/`, `deploy/crds.yaml`, and chart CRDs.
## Step 4: Add validation
File: `pkg/apis/configuration/validation/policy.go`
- Add `validate<MyPolicy>(spec *v1.MyPolicy, fieldPath *field.Path) field.ErrorList`
- Wire into `validatePolicySpec()` with field count increment and feature gate check
- Add tests in `policy_test.go` with valid and invalid cases
## Step 5: Add template structs
File: `internal/configs/version2/http.go`
- Add struct (e.g., `type MyPolicyConfig struct { ... }`)
- Add `*MyPolicyConfig` or fields to `Server`, `Location`, or both
- If the policy needs HTTP-level directives (zones, maps), add fields to `VirtualServerConfig`
## Step 6: Add config generation
File: `internal/configs/policy.go`
- Add field(s) to `policiesCfg`
- Add `add<MyPolicy>Config()` method following the pattern below
- Wire into the `switch` in `generatePolicies()`
- Add tests in `policy_test.go`
## Step 7: Wire into VirtualServer generation
File: `internal/configs/virtualserver.go`
- In `GenerateVirtualServerConfig()`, extract from `policiesCfg` and assign to `version2` fields
- Use `addPoliciesCfgToLocation()` for location-level assignment
## Step 8: Wire into Ingress generation (if applicable)
File: `internal/configs/ingress.go`
- In `generateNginxCfg()`, extract from `policiesCfg` and assign to `version1` fields
- Handle mergeable ingress in `generateNginxCfgForMergeableIngresses()`
## Step 9: Add NGINX template directives
- Version 2: `internal/configs/version2/nginx.virtualserver.tmpl` and `internal/configs/version2/nginx-plus.virtualserver.tmpl`
- Version 1: `internal/configs/version1/nginx.ingress.tmpl` and `internal/configs/version1/nginx-plus.ingress.tmpl`
- Use `{{- if }}` / `{{- with }}` guards around directive blocks
- Template helpers go in `internal/configs/version2/template_helper.go` and/or `internal/configs/version1/template_helper.go`, matching the template version you are updating
- HTTP-level directives (zones, maps) go BEFORE `server{}`
- Server-level inside `server{}`, location-level inside each `location{}`
## Step 10: Update snapshot tests
Files: `internal/configs/version2/templates_test.go` (VS/VSR/TS), `internal/configs/version1/template_test.go` (Ingress)
1. Add the new policy fields to the fixture structs used by the snapshot tests -- a regeneration with no fixture change produces no diff and leaves the policy untested.
2. Run `make test-update-snaps`.
3. `git diff -- '**/__snapshots__/**'` and confirm your directives render in the golden files for every edition the policy supports. Plus-only policies (OIDC, WAF) must appear in the Plus golden files **only**; policies available to both editions must appear in both.
4. Run `make test` to confirm green, and commit the regenerated golden files with the template change.
If you wired the policy into Ingress (Step 8), version1 snapshots must change too.
## Step 11: Update the Helm chart (if policy needs CLI flag or ConfigMap entry)
- `charts/nginx-ingress/values.yaml` -- add value with `##` doc
- `charts/nginx-ingress/values.schema.json` -- add schema entry
- `charts/nginx-ingress/templates/_helpers.tpl` -- add CLI arg or ConfigMap key
- `charts/tests/testdata/` -- add test values file
- `charts/tests/helmunit_test.go` -- add test case
## Step 12: Add controller support
File: `internal/k8s/`
- In `syncPolicy()`, ensure the new type is handled for VS/VSR/Ingress
- Check if it needs feature-gate guarding (isPlus, enableOIDC, etc.)
If the policy references secrets:
- Add every Secret field to `policySecretIndexFunc()` through `collectPolicySecretRefs()` or `collectWAFSecretRefs()`.
- Resolve each reference during extended-resource construction with `secretStore.GetSecret(namespacedKey, role)`.
- Store the result under `secrets.RefKey(namespacedKey, role)`.
- Select the role from the reference site's semantics; never infer it from `Secret.type` or Secret data.
- Ensure `syncPolicy()` fans out to every supported VS, VSR, and Ingress consumer.
- Add index tests covering add, update, delete, cross-namespace references, and duplicate references.
## Step 13: Write integration tests
Directory: `tests/suite/`
- Create test data YAMLs in `tests/data/<feature>/`
- Create `test_<feature>_policies_vs.py`, `_vsr.py`, `_ingress.py`
- Use `@pytest.mark.policies` and `@pytest.mark.policies_<feature>` markers
- Register the new marker in `pyproject.toml` -- pytest runs with `--strict-markers`
---
## Gotchas
- **Never** skip `make update-codegen` after changing `types.go` -- the build will fail with missing DeepCopy methods
- **Never** use raw user strings in NGINX config without `containsDangerousChars()` validation
- Both OSS and Plus templates must be updated for policies available to both editions -- they are separate files, each with its own snapshot entries. Plus-only policies (OIDC, WAF) belong in the Plus templates only
- A policy that reaches a template but has no snapshot fixture ships with zero rendered-output coverage
- `make update-crds` also refreshes `deploy/crds*.yaml` and `docs/crd/`; `charts/nginx-ingress/crds` is a symlink to `config/crd/bases/`
- If the policy adds telemetry counters, run `make telemetry-schema` -- CI fails on any diff in `internal/telemetry`
- `policiesCfg` duplicate check must warn and return, not error (exception: `addCORSConfig` has no duplicate check -- it overwrites, since CORS is additive via headers)
---
## Policy add*Config() Pattern
Every `add*Config()` method in `internal/configs/policy.go` follows this pattern:
```go
func (p *policiesCfg) addMyPolicyConfig(spec *conf_v1.MyPolicy, key, namespace string,
secretRefs map[secrets.SecretRefKey]*secrets.SecretReference) *validationResults {
res := newValidationResults()
// 1. Duplicate check
if p.MyPolicy != nil {
res.addWarningf("MyPolicy policy already configured, ignoring")
return res
}
// 2. Secret resolution (if applicable)
secretKey := namespace + "/" + spec.Secret
refKey := secrets.RefKey(secretKey, secrets.RoleExpected)
secretRef, ok := secretRefs[refKey]
if !ok || secretRef == nil {
res.isError = true
res.addWarningf("secret %s could not be resolved", secretKey)
return res
}
if secretRef.Error != nil {
res.isError = true
res.addWarningf("secret %s is invalid: %v", secretKey, secretRef.Error)
return res
}
// 3. Build template struct and assign
p.MyPolicy = &version2.MyPolicyConfig{
Field1: spec.Field1,
Field2: spec.Field2,
Secret: secretRef.Path,
}
return res
}
```
## NGINX Template Pattern
```nginx
{{- with $s.MyPolicy }}
my_directive {{ .Value }};
{{- if .OptionalField }}
my_optional_directive {{ .OptionalField }};
{{- end }}
{{- end }}
```
More agent context in nginx/kubernetes-ingress
11 other files this repository gives its agents.
AGENTS.md
CLAUDE.md
Copilot instructions
Skill
- nic-add-feature.github/skills/nic-add-feature/SKILL.md
- nic-ci-pipelines.github/skills/nic-ci-pipelines/SKILL.md
- nic-code-review.github/skills/nic-code-review/SKILL.md
- nic-debugging.github/skills/nic-debugging/SKILL.md
- nic-docker-images.github/skills/nic-docker-images/SKILL.md
- nic-planning.github/skills/nic-planning/SKILL.md
- nic-structure.github/skills/nic-structure/SKILL.md
- nic-testing.github/skills/nic-testing/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
No reports yet. Be the first to say whether it worked.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool registry_write, action report. How to connect one.

