K8s Access Control: Structured Authentication Configuration
Why it matters?
Structured Authentication Configuration allows for a more flexible and extensible way to configure authentication in Kubernetes.
Supports:
- Multiple JWT authenticators simultaneously (that produce any JWT-compliant token)
- Before this feature, the apiserver only supported one OIDC provider. External solution like Dex enabled multiple providers.
- Multiple audiences: Use the same authenticator for multiple audiences
- such as kubectl and a dashboard accessing the apiserver. read more here
- Using identity providers that don't support OpenID connect discovery
- Changing the configuration without restarting the API server
- Use of CEL (Common Expression Language) to write logic for claim mappings and custom validation rules.
The big picture:
OIDC is a widely adopted and secure authentication standard. While Kubernetes has supported OIDC authentication since 2015, its OIDC authenticator has historically suffered from limited test coverage, hindering feature development. Key limitations include strict token validation, flag argument only configuration, and single provider support.
This new feature, released as Alpha since v1.29, and now Beta since v1.30, serves as the next iteration of the existing OIDC authenticator. In my previous post, I discussed the OIDC authenticator; see that post to learn more.
For more detail on the motivation and goals of the proposal, see SIG-Auth's KEP-3331 | 2023 KubeCon NA Session
How it works
Key Components
- kube-apiserver authentication configuration file
- kubeconfig setup
- identity provider integration
Prerequisites
- Compute, in my case I'm playing with Iximiuz Labs - remote DevOps playgrounds
- Kind v0.25.0 single node cluster (k8s v1.30+)
- Kubectl v1.31.3
- Two OIDC Identity Providers, I'm using Auth0's (free) development instances
Kube-apiserver configuration
A new --authentication-config
flag for kube-apiserver allows specifying a YAML file that defines JWT authenticators and their configuration, providing a more structured approach to authentication configuration.
Note: Specifying both --authentication-config
and --oidc
command line arguments is a misconfiguration.
The structured configuration file holds a slice of type JWTAuthenticator which contains the following fields.
Config file references: K8s docs | K8s Blog | Source code
- issuer: external OIDC provider specific settings
- claimValidationRules: rules that are applied to validate token claims to authenticate users
- claimMappings: points claims of a token to be treated as user attributes
- userValidationRules: rules that are applied to final user info before completing authentication
Here is a condensed sample of the config file for brevity:
---
apiVersion: apiserver.config.k8s.io/v1beta1
kind: AuthenticationConfiguration
# list of authenticators to authenticate Kubernetes users using JWT compliant tokens.
jwt: # a list of OIDC providers to authenticate Kubernetes users
- issuer:
url: https://example-one.com
discoveryURL: https://discovery.example-one.com/.well-known/openid-configuration
certificateAuthority: |
-----BEGIN CERTIFICATE-----
xxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxx
-----END CERTIFICATE-----
audiences:
- my-app-clied-id
- my-other-app-client-id
audienceMatchPolicy: MatchAny
claimValidationRules: [...]
claimMappings: {...}
userValidationRules: [...]
- issuer:
url: https://example-two.com
discoveryURL: https://discovery.example-two.com/.custom-known/openid-config
certificateAuthority: <PEM encoded CA certificates>
audiences:
- my-production-app
audienceMatchPolicy: MatchAny
claimValidationRules: [...]
claimMappings: {...}
userValidationRules: [...]
Unpacking a Use Case
Two important features that make structured authentication flexible and extensible:
- multiple IdP issuers
- custom validation rules with CEL.
Auth0's IdP hackery
Disclaimer: For demonstration purposes, I'm simplifying and simulating certain IdP workflows behind the scenes to focus on the structured authentication features. These simulations might not reflect real-world production scenarios, and I may unintentionally misrepresent certain concepts. If you spot any inaccuracies, kindly correct me so I can improve the content. Reach out to labs@alandiaz.com.
I've created two Auth0 dev instances: Why? to see the multiple JWT authenticators in action
- Amapiano-IdP: will act as a special entitlements IdP
- simulate minting JWT
id_token
's with custom claims to provide Just-in-Time (JIT) privilege access. Why? to explore the validation rules and CEL - 4 hour id token expiration
- set custom claims: e.g.
amapiano_groups: jit-edit
amapiano_ticket: S12345
- amapiano-idp .well-known/openid-configuration
- simulate minting JWT
- Salsa-IdP: will act as the main OIDC Provider for users
- mints standard JWT
id_token
's - set custom claims:
salsa_groups: priv:view
- salsa-idp .well-known/openid-configuration
- mints standard JWT
User workflow 1
Given: user jonny@runtime.diaz performs an oauth flow request for an id_token against Amapiano-IdP
When: the request includes the following scopes, an (approved) service ticket (e.g. S12345
) and the privileged role needed (e.g. role:edit
)
Then: the id_token
is validated by k8s' structured authN config and user and group attributes are correctly processed based off the validation stages set 1
Sample JWT id_token
# Encoded
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1MazVvTFEwVWxNSDNaWDZLcWcwWiJ9.eyJhbWFwaWFub19ncm91cHMiOiJqaXQtZWRpdCIsImFtYXBpYW5vX3RpY2tldCI6IlMxMjM0NSIsImFtYXBpYW5vX25iZiI6MTczNDA0MzQ0MCwiZW1haWwiOiJqb25ueUBydW50aW1lLmRpYXoiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6Ly9hbWFwaWFuby1pZHAudXMuYXV0aDAuY29tLyIsImF1ZCI6ImpMZTdSRDRNcWFXNmZGZ1J3T1hhOEIwbjNPVkZ0NFo3IiwiaWF0IjoxNzM0MDQzNDQwLCJleHAiOjE3MzQwNTc4NDAsInN1YiI6ImF1dGgwfDY3NTkwZTU1YzU1YTE3MjdkNjc3YTI0YSJ9.qZTv42Stqn-htmojYlDsoZezU1SJhQvxRtSQsi01XdAyL2kcd_jBkUB7ODJDpArOc-P4IWRr3VkclB99P8QqQG6eaPjJS8tHiU7qUA0pJmc64Km_e3oopuwdTyHYfoNEyCnyBhRkQzSFCb0l1NbsmYx5BobrGxjbY8kea12OcYwfcp15zdlCEmSY1obmPc7vo8TnLq8qrgaSQUXelmEc6-iomj_MMOg-UP44t3TJAY32CrVgJ6TvCZCl-l9SejLG6zoUdZRfhA4SqBD9mcYTfnsOFaLpiAqxNyk_ARxy05a6MmqC2Wj3h3cCXARc0oa4Kv9_U8NMf-PGf2WL-p1JCQ
# Decoded
{
"alg": "RS256",
"typ": "JWT",
"kid": "mLk5oLQ0UlMH3ZX6Kqg0Z"
}
{
"amapiano_groups": "jit-edit",
"amapiano_ticket": "S12345",
"amapiano_nbf": 1734043440,
"email": "jonny@runtime.diaz",
"email_verified": true,
"iss": "https://amapiano-idp.us.auth0.com/",
"aud": "jLe7RD4MqaW6fFgRwOXa8B0n3OVFt4Z7",
"iat": 1734043440,
"exp": 1734057840,
"sub": "auth0|67590e55c55a1727d677a24a"
}
[Signature]
User workflow 2
Given: user jonny@runtime.diaz performs an oauth flow request for an id_token against the Salsa-IdP
When: the request includes the openid
scope only and no privileged access
Then: the id_token
is validated by k8s' structured authN config and user and group attributes are correctly processed based off the validation stages set 2
Validation stages set 1
Validation stages set 2
Zoom into validations:
Custom claim validation rules are applied after generic OIDC validations, e.g., checking the token signature, issuer URL, etc. Cel validation expressions must always evaluate a boolean. The KEP does a good job breaking down key CEL behavior.
NOTE: One variable will be available to use in claimValidationRules and claimMappings:
claims
for JWT claims (payload)
NOTE: If an expression returns false after evaluation, a 401 Unauthorized error will be returned to the user and the associated message will be logged in the API server logs.
claimValidationRules:
- expression: |
has(claims.amapiano_ticket) && claims.amapiano_ticket != "" &&
has(claims.amapiano_groups) && claims.amapiano_groups != ""
message: the amapiano_ticket and amapiano_groups claims must exist and not be empty
- expression: 'has(claims.amapiano_nbf) && claims.amapiano_nbf != "" && claims.exp - claims.amapiano_nbf <= 21600'
message: total token lifetime must not exceed 6 hours
Claim mapping rules (and optional transformations) to map claims from a token to Kubernetes user info attributes.
claimMappings:
username:
claim: "email"
prefix: ""
groups:
expression: claims['amapiano_groups']
uid:
claim: 'sub'
User attribute validations apply more validations and/or transformations to the attributes mapped to user info.
NOTE: One variable will be available to use in userValidationRules - user with the same schema as authentication.k8s.io/v1, Kind=UserInfo
userValidationRules:
- expression: "!user.username.startsWith('system:')"
message: 'username cannot used reserved system: prefix'
- expression: "user.groups.all(group, !group.startsWith('system:'))"
message: 'groups cannot used reserved system: prefix'
Try it out
This Free Playground will fire up the ephemeral Kind Cluster: Structured AuthN Config playground. Once fully initialized, execute the scripts below from the home directory. Once executed, each script's output will provide further instruction.
User workflow 1
./k8s_user_auth_init.sh \
--oauth-token-url "https://amapiano-idp.us.auth0.com/oauth/token" \
--idp-issuer-url "https://amapiano-idp.us.auth0.com/" \
--username "jonny@runtime.diaz" \
--password "Demo12345" \
--client-id "jLe7RD4MqaW6fFgRwOXa8B0n3OVFt4Z7" \
--client-secret "BCSXWL1IrhHz2WgqT1FLGpAxa6QNRL-6He5IJgSLxP65uV1PAcccjx_5knRGFTLH" \
--scope "openid email role:edit S12345"
User workflow 2
./k8s_user_auth_init.sh \
--oauth-token-url "https://salsa-idp.us.auth0.com/oauth/token" \
--idp-issuer-url "https://salsa-idp.us.auth0.com/" \
--username "jonny@runtime.diaz" \
--password "Demo12345" \
--client-id "u2vEGqwWnlf2QtXu56dhWOCwydMtY2n5" \
--client-secret "_l4GmT25-rV21wNoD_gcxXgm42C1ZWbtvZXUykIjoEyNypUeCIq9B2pTzHwUy5no" \
--scope "openid email"
What's Next
- Look into auditing and logging authN workflows
- Updates on Authorization controls
- Updates on Admission controls