Runtime.Diaz

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:

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

  1. kube-apiserver authentication configuration file
  2. kubeconfig setup
  3. identity provider integration

Prerequisites

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

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:

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

  1. 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
  2. Salsa-IdP: will act as the main OIDC Provider for users

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

set1

Validation stages set 2

set2

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

#authentication #kubernetes #oauth2 #oidc