User guide: Preventing namespace-to-cluster privilege escalation (AuthConfigs)¶
Two fields on AuthConfig resources reach beyond the namespace they live in: spec.authentication.*.apiKey.allNamespaces and spec.authentication.*.x509.allNamespaces. When either is set to true, cluster-wide Authorino instances will look up the API-key / trusted-certificate Secrets across every namespace in the cluster, so anyone allowed to create AuthConfigs in a single namespace can use Authorino's elevated privileges to quietly reach secrets at cluster scope.
This issue does not affect namespaced Authorino instances, but it can be a problem in multi-tenant, shared Authorino instances (aka: cluster-wide deployments).
The ValidatingAdmissionPolicy below closes that gap. It blocks every create and update of an AuthConfig that has one of those fields enabled, unless the user has been given a special permission for it, and you hand that permission only to the subjects that need it to do their job.
The policy:
| Policy | Resource | Denies | ClusterRole required to allow |
|---|---|---|---|
authconfig-restrict-all-namespaces |
authconfigs |
spec.authentication.*.apiKey.allNamespaces: true |
set-apikey-all-namespaces on authconfigs |
spec.authentication.*.x509.allNamespaces: true |
set-x509-all-namespaces on authconfigs |
Follow the steps below: create the Roles that grant those permissions, bind the roles to specific SAs and Users, then apply the policy.
1. Create the Roles¶
kubectl apply -f - <<'EOF'
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: authorino-set-apikey-all-namespaces
rules:
- apiGroups: ["authorino.kuadrant.io"]
resources: ["authconfigs"]
verbs: ["set-apikey-all-namespaces"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: authorino-set-x509-all-namespaces
rules:
- apiGroups: ["authorino.kuadrant.io"]
resources: ["authconfigs"]
verbs: ["set-x509-all-namespaces"]
EOF
2. Grant the access to the restricted fields¶
Grant access to your own ServiceAccounts and Users. Use the RoleBindings below as a template. Replace the placeholders (<sa-name>, <namespace-of-sa>, <authconfig-namespace>) with the appropriate values.
kubectl apply -f - <<'EOF'
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: rb-set-apikey-all-namespaces
namespace: <authconfig-namespace>
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: authorino-set-apikey-all-namespaces
subjects:
- kind: ServiceAccount
name: <sa-name>
namespace: <namespace-of-sa>
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: rb-set-x509-all-namespaces
namespace: <authconfig-namespace>
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: authorino-set-x509-all-namespaces
subjects:
- kind: ServiceAccount
name: <sa-name>
namespace: <namespace-of-sa>
EOF
3. Create the ValidatingAdmissionPolicy (VAP)¶
kubectl apply -f - <<'EOF'
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: authconfig-restrict-all-namespaces
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["authorino.kuadrant.io"]
apiVersions: ["v1beta3"]
operations: ["CREATE", "UPDATE"]
resources: ["authconfigs"]
variables:
- name: isExemptApiKey
expression: "authorizer.requestResource.check('set-apikey-all-namespaces').allowed()"
- name: isExemptX509
expression: "authorizer.requestResource.check('set-x509-all-namespaces').allowed()"
- name: wantsApiKeyAllNamespaces
expression: "has(object.spec.authentication) && object.spec.authentication.exists(k, has(object.spec.authentication[k].apiKey) && has(object.spec.authentication[k].apiKey.allNamespaces) && object.spec.authentication[k].apiKey.allNamespaces)"
- name: wantsX509AllNamespaces
expression: "has(object.spec.authentication) && object.spec.authentication.exists(k, has(object.spec.authentication[k].x509) && has(object.spec.authentication[k].x509.allNamespaces) && object.spec.authentication[k].x509.allNamespaces)"
validations:
- expression: "!variables.wantsApiKeyAllNamespaces || variables.isExemptApiKey"
message: "apiKey allNamespaces: true (cluster-wide secret lookup) requires the 'set-apikey-all-namespaces' permission on authconfigs; a subject without it can neither create nor modify an AuthConfig that has the field enabled"
reason: Forbidden
- expression: "!variables.wantsX509AllNamespaces || variables.isExemptX509"
message: "x509 allNamespaces: true (cluster-wide secret lookup) requires the 'set-x509-all-namespaces' permission on authconfigs; a subject without it can neither create nor modify an AuthConfig that has the field enabled"
reason: Forbidden
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: authconfig-restrict-all-namespaces-binding
spec:
policyName: authconfig-restrict-all-namespaces
validationActions: ["Deny"]
EOF
Warning
The policy restricts having a restricted field enabled, not merely the act of enabling it. Any request — create or update — whose resulting resource sets apiKey.allNamespaces: true or x509.allNamespaces: true is rejected unless the requesting subject holds the matching permission. In practice this makes an AuthConfig that enables a restricted field read-only for every subject that lacks the permission: they cannot edit even unrelated fields such as hosts or authorization. They can still submit an update that disables the field, and resources that leave the restricted fields unset or false are unaffected. Grant the Roles and RoleBindings from steps 1–2 to every subject that has to write these resources, including controllers and GitOps agents that reconcile them.
The policy deliberately does not grandfather resources that already enable a restricted field. Editing the rest of the spec is enough to abuse the cluster-wide lookup: the resolved identity of an apiKey authentication is the matched Secret itself, data included, and spec.callbacks and spec.response can read auth.identity. A subject able to edit an already cluster-wide AuthConfig could therefore attach a callback to a destination it controls and have every successful request forward the caller's Secret, from any namespace in the cluster, without ever touching allNamespaces or its selector.
Existing resources are not deleted or rewritten when you apply the policy, and Authorino keeps enforcing them. Only subsequent writes to them are gated.
Before rolling the policy out on a live cluster, list the resources that will become restricted, so you know which subjects need the permission from steps 1–2:
kubectl get authconfigs -A -o json | jq -r '
.items[]
| select(
[.spec.authentication[]? | (.apiKey.allNamespaces // false) or (.x509.allNamespaces // false)]
| any
)
| "\(.metadata.namespace)/\(.metadata.name)"'
4. Verifying the VAP¶
A normal user is blocked¶
Try to create resources that break the rules. Run these as a regular user (one without the permissions) and both should be rejected:
Note
Do not run these as a cluster administrator. Anything with wildcard access (verbs: ["*"]) — which cluster admins have — satisfies the set-apikey-all-namespaces / set-x509-all-namespaces checks and is treated as exempt, so the request would be allowed and a real cluster-wide secret lookup enabled. Use an ordinary user (or --as=<unauthorized-subject>) to see the policy block.
# AuthConfig with apiKey allNamespaces: true — should be DENIED
kubectl apply --as=<unauthorized-subject> -f - <<'EOF'
apiVersion: authorino.kuadrant.io/v1beta3
kind: AuthConfig
metadata:
name: policy-all-namespaces-1
namespace: <namespace>
spec:
hosts:
- test-denied.example.com
authentication:
api-key-users:
apiKey:
allNamespaces: true
selector:
matchLabels:
group: friends
EOF
# AuthConfig with x509 allNamespaces: true — should be DENIED
kubectl apply --as=<unauthorized-subject> -f - <<'EOF'
apiVersion: authorino.kuadrant.io/v1beta3
kind: AuthConfig
metadata:
name: policy-x509-1
namespace: <namespace>
spec:
hosts:
- test-x509-denied.example.com
authentication:
mtls-clients:
x509:
allNamespaces: true
selector:
matchLabels:
group: friends
EOF
You should get an error like this instead of the resource being created:
... is forbidden: ValidatingAdmissionPolicy 'authconfig-restrict-all-namespaces' ... denied request: apiKey allNamespaces: true (cluster-wide secret lookup) requires the 'set-apikey-all-namespaces' permission on authconfigs; a subject without it can neither create nor modify an AuthConfig that has the field enabled
A permitted subject is allowed¶
Now run the same requests as a subject that holds the matching permission (granted in steps 1–2). Both should be admitted. Replace <authorized-subject> with the subject you granted the permission to (for example, system:serviceaccount:<namespace>:<sa>):
# AuthConfig with apiKey allNamespaces: true, as a subject granted 'set-apikey-all-namespaces' — should be ALLOWED
kubectl apply --as=<authorized-subject> -f - <<'EOF'
apiVersion: authorino.kuadrant.io/v1beta3
kind: AuthConfig
metadata:
name: policy-all-namespaces-2
namespace: <namespace>
spec:
hosts:
- test-allowed.example.com
authentication:
api-key-users:
apiKey:
allNamespaces: true
selector:
matchLabels:
group: friends
EOF
# AuthConfig with x509 allNamespaces: true, as a subject granted 'set-x509-all-namespaces' — should be ALLOWED
kubectl apply --as=<authorized-subject> -f - <<'EOF'
apiVersion: authorino.kuadrant.io/v1beta3
kind: AuthConfig
metadata:
name: policy-x509-2
namespace: <namespace>
spec:
hosts:
- test-x509-allowed.example.com
authentication:
mtls-clients:
x509:
allNamespaces: true
selector:
matchLabels:
group: friends
EOF
Resources without the restricted fields are always allowed¶
The policy only looks at the restricted fields. A resource that leaves them unset (or false) is admitted for any subject, whether or not it holds a permission:
# AuthConfig with apiKey allNamespaces: false — should be ALLOWED even for an unauthorized subject
kubectl apply --as=<unauthorized-subject> -f - <<'EOF'
apiVersion: authorino.kuadrant.io/v1beta3
kind: AuthConfig
metadata:
name: policy-namespaced-1
namespace: <namespace>
spec:
hosts:
- test-namespaced.example.com
authentication:
api-key-users:
apiKey:
allNamespaces: false
selector:
matchLabels:
group: friends
EOF
Updates are re-checked, not just creates¶
Because the policy matches UPDATE as well as CREATE, it re-evaluates on every change, against the resource as it would be after the update. A subject without the permission can freely edit a resource that leaves the restricted fields off, but cannot submit any update whose result has one of them on. Using the namespaced AuthConfig created above:
# Change an unrelated field (the apiKey selector) on the namespaced AuthConfig — should be ALLOWED
kubectl apply --as=<unauthorized-subject> -f - <<'EOF'
apiVersion: authorino.kuadrant.io/v1beta3
kind: AuthConfig
metadata:
name: policy-namespaced-1
namespace: <namespace>
spec:
hosts:
- test-namespaced.example.com
authentication:
api-key-users:
apiKey:
allNamespaces: false
selector:
matchLabels:
group: family
EOF
# Flip the same AuthConfig to apiKey allNamespaces: true — should be DENIED
kubectl apply --as=<unauthorized-subject> -f - <<'EOF'
apiVersion: authorino.kuadrant.io/v1beta3
kind: AuthConfig
metadata:
name: policy-namespaced-1
namespace: <namespace>
spec:
hosts:
- test-namespaced.example.com
authentication:
api-key-users:
apiKey:
allNamespaces: true
selector:
matchLabels:
group: family
EOF
An object that already has a restricted field enabled is closed to subjects without the permission, even for edits that do not touch the field. Using policy-all-namespaces-2 (created by the authorized subject above, with apiKey.allNamespaces: true):
# Edit an unrelated field (the host) while leaving apiKey allNamespaces: true unchanged — should be DENIED
kubectl apply --as=<unauthorized-subject> -f - <<'EOF'
apiVersion: authorino.kuadrant.io/v1beta3
kind: AuthConfig
metadata:
name: policy-all-namespaces-2
namespace: <namespace>
spec:
hosts:
- test-allowed-updated.example.com
authentication:
api-key-users:
apiKey:
allNamespaces: true
selector:
matchLabels:
group: friends
EOF
The one update such a subject can still make is one that turns the restricted field off, since the resulting resource no longer enables the cluster-wide lookup:
# Disable apiKey allNamespaces on the same resource — should be ALLOWED
kubectl apply --as=<unauthorized-subject> -f - <<'EOF'
apiVersion: authorino.kuadrant.io/v1beta3
kind: AuthConfig
metadata:
name: policy-all-namespaces-2
namespace: <namespace>
spec:
hosts:
- test-allowed.example.com
authentication:
api-key-users:
apiKey:
allNamespaces: false
selector:
matchLabels:
group: friends
EOF
Permissions bound with a RoleBinding are namespace-scoped¶
The exemption check runs against the namespace of the resource being admitted. If you grant the permission with a RoleBinding (rather than a ClusterRoleBinding), the subject is exempt only in that namespace.
# Subject granted 'set-apikey-all-namespaces' via a RoleBinding in <namespace-a> — should be ALLOWED
kubectl apply --as=<authorized-subject> -f - <<'EOF'
apiVersion: authorino.kuadrant.io/v1beta3
kind: AuthConfig
metadata:
name: policy-all-namespaces-3
namespace: <namespace-a>
spec:
hosts:
- test-rb-a.example.com
authentication:
api-key-users:
apiKey:
allNamespaces: true
selector:
matchLabels:
group: friends
EOF
# Same subject, same request, in <namespace-b> where it has no binding — should be DENIED
kubectl apply --as=<authorized-subject> -f - <<'EOF'
apiVersion: authorino.kuadrant.io/v1beta3
kind: AuthConfig
metadata:
name: policy-all-namespaces-3
namespace: <namespace-b>
spec:
hosts:
- test-rb-b.example.com
authentication:
api-key-users:
apiKey:
allNamespaces: true
selector:
matchLabels:
group: friends
EOF