The NSI Authentication Server (nsi-auth) integrates with Kubernetes ingress
controllers and gateways such as ingress-nginx, Traefik and Envoy
Gateway. It is an external authorization endpoint: the proxy forwards each
request's client-certificate identity to nsi-auth, which extracts the
Distinguished Name (DN) and verifies it against an allow-list using
standards-compliant RFC 4514 comparison (via Python cryptography
x509.Name).
nsi-auth reads the identity from one configurable header
(TLS_CLIENT_SUBJECT_AUTHN_HEADER) and parses it with one explicitly
configured codec (TLS_CLIENT_AUTHN_FORMAT). The two are independent, so any
proxy / header combination works. There are two capabilities:
- Compare a DN that the proxy already extracted and put in a header, or
- Extract the DN from a certificate carried in a header.
TLS_CLIENT_AUTHN_FORMAT |
Reads | Typical proxy / header |
|---|---|---|
dn-rfc2253 |
a DN string (RFC 2253) | ingress-nginx ssl-client-subject-dn; Envoy Lua |
traefik-info |
DN inside Subject="…" |
Traefik X-Forwarded-Tls-Client-Cert-Info |
traefik-pem |
Traefik minimized PEM (chain) | Traefik X-Forwarded-Tls-Client-Cert |
pem |
a standard (URL-encoded) PEM | nginx ssl-client-cert; any proxy |
xfcc-cert |
the PEM in XFCC Cert= |
Envoy x-forwarded-client-cert |
xfcc-subject |
the DN in XFCC Subject= |
Envoy x-forwarded-client-cert |
There is no fallback between codecs: if the configured source is missing or
unparseable, nsi-auth fails closed (403) rather than silently using a
different field.
- ✅ If the DN is authorized → HTTP 200 (OK) with
X-Auth-Method: mTLSandX-Client-DN: <RFC 4514 DN>response headers - ❌ Otherwise → HTTP 403 (Forbidden)
The proxy forwards these response headers to the upstream application via
auth-response-headers (nginx), authResponseHeaders (Traefik), or
extAuth.http.headersToBackend (Envoy Gateway), so downstream services (e.g.
nsi-dds-proxy) can confirm mTLS happened and identify the client.
nsi-auth is deployed via a Helm chart. The full list of configuration options
can be found in chart/values.yaml.
Below is an example configuration snippet:
image:
repository: ghcr.io/workfloworchestrator/nsi-auth
pullPolicy: IfNotPresent
tag: "latest"
service:
type: ClusterIP
port: 80
targetPort: 8000
volumes:
- name: config
configMap:
name: nsi-auth-config
optional: false
volumeMounts:
- name: config
mountPath: "/config"
readOnly: true
livenessProbe:
httpGet:
path: /health
port: http
readinessProbe:
httpGet:
path: /health
port: http
env:
ALLOWED_CLIENT_SUBJECT_DN_PATH: "/config/allowed_client_dn.txt"
TLS_CLIENT_SUBJECT_AUTHN_HEADER: "ssl-client-subject-dn"
USE_WATCHDOG: "False"
LOG_LEVEL: "INFO"
config:
inlineData: |-
allowed_client_dn.txt: |-
CN=CertA,OU=Dept X,O=Company 1,C=NL
CN=CertB,OU=Dept Y,O=Company 2,C=NL
CN=CertC,OU=Dept Z,O=Company 3,C=NL
additionalTrustedCA: ""You can override default values by passing a custom values file:
helm upgrade --install --values my-values.yaml nsi-auth chartNote:
The value
configMap.nameis defined as{{ .Release.Name }}-configand must match your Helm release name. In this example, the release name is nsi-auth.
Alternatively, install directly from the nsi-node Helm repository:
helm repo add nsi-node https://bandwidthondemand.github.io/nsi-node/
helm repo update
helm upgrade --install --values my-values.yaml nsi-auth nsi-node/nsi-authDuring installation, a Kubernetes secret named {{ .Release.Name }}-ca is
automatically created. This secret contains a ca.crt file, which includes:
- The list of CA certificates maintained by cURL
- Any additional certificates defined under
config.additionalTrustedCA
This allows you to extend the trusted CA list with other certificates, including self-signed CAs if needed.
The ca.crt secret is then used by the ingress controller to establish the
trusted CA chain (see Ingress Configuration).
| Variable | Description | Default |
|---|---|---|
ALLOWED_CLIENT_SUBJECT_DN_PATH |
Path to the file listing allowed client certificate DNs. DNs should be as close to RFC 4514 format as possible, stored as UTF-8. | /config/allowed_client_dn.txt |
TLS_CLIENT_SUBJECT_AUTHN_HEADER |
Name of the HTTP header carrying the client identity. Any header name; must match what the proxy sends. | ssl-client-subject-dn |
TLS_CLIENT_AUTHN_FORMAT |
Codec used to parse that header's value into a DN: dn-rfc2253, traefik-info, traefik-pem, pem, xfcc-cert, or xfcc-subject (see the table in the Overview). No fallback — a missing/unparseable source returns 403. |
dn-rfc2253 |
USE_WATCHDOG |
Enables file-change monitoring using watchdog. Useful for non-Kubernetes environments. | False |
LOG_LEVEL |
Logging verbosity. Options: DEBUG, INFO, WARNING, ERROR. |
INFO |
Health endpoint:
nsi-auth exposes a /health endpoint that returns HTTP 200. This is used
for Kubernetes liveness and readiness probes, which are enabled by default in
the Helm chart:
livenessProbe:
httpGet:
path: /health
port: http
readinessProbe:
httpGet:
path: /health
port: httpFile reload behavior:
By default, nsi-auth uses a simple polling mechanism (every 5 seconds) to
detect changes to the DN file. If USE_WATCHDOG is enabled, the watchdog
module provides faster, event-based file monitoring.
⚠️ Note:watchdogcannot be used when running in Kubernetes, because ConfigMap updates replace the file via symbolic links, and this is not detected.
DN format and comparison:
DNs are parsed per RFC 4514 (via cryptography.x509.Name) and compared as
the multiset of (attribute-OID, value) pairs.
This makes matching independent of:
- RDN ordering —
CN=Foo,O=Acme,C=NLandC=NL,O=Acme,CN=Fooare treated as the same identity. - Attribute-type spelling — friendly names and dotted OIDs match each
other (
emailAddress=≡1.2.840.113549.1.9.1=,GN=≡2.5.4.42=,SN=≡2.5.4.4=,organizationIdentifier=≡2.5.4.97=). This is important because different reverse proxies serialize the same DN differently (e.g. Go'scert.Subject.String()falls back to dotted OIDs for any attribute type it doesn't have a friendly name for).
DNs in the allowed DN file should be as close to
RFC 4514 format as possible
(e.g. CN=CertA,OU=Dept X,O=Company 1,C=NL). The file must be UTF-8
encoded.
Finally, configure the ingress controller of the application to:
- Use the
ca.crtsecret created bynsi-auth - Enable and verify mutual TLS (mTLS) authentication
- Forward the client certificate DN (or certificate) to
nsi-authfor validation
Keep the defaults TLS_CLIENT_SUBJECT_AUTHN_HEADER: ssl-client-subject-dn and
TLS_CLIENT_AUTHN_FORMAT: dn-rfc2253.
Assuming nsi-auth is deployed in the production namespace, use the
following ingress annotations:
nginx.ingress.kubernetes.io/auth-tls-secret: production/nsi-auth-ca
nginx.ingress.kubernetes.io/auth-tls-verify-client: "on"
nginx.ingress.kubernetes.io/auth-tls-verify-depth: "3"
nginx.ingress.kubernetes.io/auth-url: http://nsi-auth.production.svc.cluster.local/validateTo forward the full PEM certificate (or chain), set:
TLS_CLIENT_SUBJECT_AUTHN_HEADER: X-Forwarded-Tls-Client-Cert
TLS_CLIENT_AUTHN_FORMAT: traefik-pemand enable the Traefik
PassTLSClientCert
middleware with pem: true:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: nsi-auth-mtls
spec:
passTLSClientCert:
pem: trueAlternatively, use the cert-info summary header — set
TLS_CLIENT_SUBJECT_AUTHN_HEADER: X-Forwarded-Tls-Client-Cert-Info and
TLS_CLIENT_AUTHN_FORMAT: traefik-info, and configure the info.subject fields
in the middleware.
Envoy Gateway has no built-in "subject DN" header. The recommended approach
mirrors ingress-nginx: inject the DN with a small Lua filter and keep the
defaults (ssl-client-subject-dn + dn-rfc2253). A best-current-practice
deployment (here nsi-auth and the protected app are in production) is four
resources:
1. Terminate mTLS on the listener — ClientTrafficPolicy validates the
client certificate against the nsi-auth CA secret:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
name: my-app-mtls
namespace: production
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: my-gateway
sectionName: https-my-app # the listener serving the app's host
tls:
clientValidation:
caCertificateRefs:
- kind: Secret
group: ""
name: nsi-auth-ca # the {{ .Release.Name }}-ca secret
mode: RequireAndVerify2. Copy the cert subject DN into the header — EnvoyExtensionPolicy (Lua),
targeting the app's HTTPRoute. It strips any client-supplied value first, so a
valid cert-holder cannot assert a different DN:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyExtensionPolicy
metadata:
name: my-app-client-cert
namespace: production
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: my-app
lua:
- type: Inline
inline: |
function envoy_on_request(request_handle)
request_handle:headers():remove("ssl-client-subject-dn")
local dsc = request_handle:streamInfo():downstreamSslConnection()
if dsc ~= nil and dsc:peerCertificatePresented() then
local subject = dsc:subjectPeerCertificate() -- RFC 2253
if subject ~= nil and subject ~= "" then
request_handle:headers():add("ssl-client-subject-dn", subject)
end
end
endsubjectPeerCertificate() returns the DN in RFC 2253 — the same format
ingress-nginx emits — so nsi-auth parses it with dn-rfc2253.
3. Run Lua before ext_authz — otherwise the header is not set when the auth
subrequest fires. Configure the EnvoyProxy referenced by your GatewayClass:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
name: my-proxy
namespace: production
spec:
filterOrder:
- name: envoy.filters.http.lua
before: envoy.filters.http.ext_authz4. Delegate authorization to nsi-auth — SecurityPolicy forwards the DN
header to /validate and passes the response headers on to the app:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
name: my-app-security-policy
namespace: production
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: my-app
extAuth:
headersToExtAuth:
- ssl-client-subject-dn # the Lua-set DN reaches nsi-auth
http:
backendRef:
kind: Service
name: nsi-auth
namespace: production
port: 80
pathOverride: /validate
headersToBackend:
- X-Auth-Method
- X-Client-DN
⚠️ Envoy's ext_authz mirrors the downstream request method onto the/validatesubrequest (it does not force GET).nsi-authaccepts any method on/validatefor this reason; do not reverse that, or every POST to a protected backend is rejected by the auth step.
Alternative — forward the certificate via XFCC. Instead of the Lua filter,
make Envoy emit the cert in x-forwarded-client-cert by setting
set_current_client_cert_details.cert: true (with
forward_client_cert_details: SANITIZE_SET) via an EnvoyPatchPolicy, then set
TLS_CLIENT_SUBJECT_AUTHN_HEADER: x-forwarded-client-cert and
TLS_CLIENT_AUTHN_FORMAT: xfcc-cert (or xfcc-subject for the Subject=
field). The Lua approach needs no raw-config patch, so it is preferred.
These settings ensure that the ingress controller:
- Validates client certificates against the trusted CA chain
- Delegates authorization to the nsi-auth service
📄 License
This project is licensed under the Apache License, Version 2.0.
🧠 See Also