Skip to content

security: disable anonymous auth on kube-apiserver (MK8S-187)#4900

Open
g-carre wants to merge 4 commits intodevelopment/132.0from
bugfix/MK8S-187-disable-kube-apiserver-anonymous-auth
Open

security: disable anonymous auth on kube-apiserver (MK8S-187)#4900
g-carre wants to merge 4 commits intodevelopment/132.0from
bugfix/MK8S-187-disable-kube-apiserver-anonymous-auth

Conversation

@g-carre
Copy link
Copy Markdown
Contributor

@g-carre g-carre commented May 4, 2026

Component: salt, tests

Context:

A security assessment flagged that the ARTESCA Kubernetes cluster permits anonymous access to the API server. With kube-apiserver running on its default --anonymous-auth=true, unauthenticated callers can reach endpoints bound to the kubeadm-managed system:public-info-viewer ClusterRoleBinding (e.g. /version, /healthz) and disclose Kubernetes / Go versions usable for further attack planning.

Reproducer (from the advisory): curl -k https://<INSTANCE_IP>:8443/api/kubernetes/version returns a JSON body with gitVersion / goVersion instead of 401.

Summary:

  • Configure kube-apiserver with an AuthenticationConfiguration (/etc/kubernetes/authentication-config.yaml, generated from a new sibling salt state mirroring cryptconfig.sls) that scopes anonymous access to /livez, /readyz, /healthz only. --anonymous-auth=false was tried first but is incompatible with kubelet httpGet probes (which cannot carry credentials) and would put the apiserver in a startup-probe crash-loop after ~250s; it is also mutually exclusive with the AuthenticationConfiguration.anonymous block, so it is dropped.
  • Migrate the OIDC issuer config from --oidc-* CLI flags into AuthenticationConfiguration.jwt. K8s 1.32 rejects the two mechanisms together (pkg/kubeapiserver/options/authentication.go: "authentication-config file and oidc- flags are mutually exclusive"*). A first apiserver start can succeed before the control-plane Ingress endpoint is known (no --oidc-* flags emitted), but the subsequent Reconfigure control plane Ingress pass populates oidc_config, re-renders the manifest with both flag sets, and the apiserver crash-loops on startup. The same five fields the legacy code consumed (issuerURL, clientID, CAFile, usernameClaim, groupsClaim) feed the new jwt issuer block. The CA PEM is read from the salt mine entry ingress_ca_b64 for the default Dex / Ingress path (avoiding any state-graph ordering hazard), with a salt['file.read'] fallback for a pillar-supplied OIDC override pointing at a different CA path. When the username claim is email, a claimValidationRules CEL expression claims.?email_verified.orValue(true) is added to reproduce the implicit email_verified == true guard that --oidc-username-claim=email used to apply automatically. ARTESCA configures Keycloak via the same OIDC pillar override (installer/artesca_installer/salt/artesca/base/keycloak/configured.sls), so its reconfiguration path is unaffected.
  • The new mechanism relies on the AnonymousAuthConfigurableEndpoints and StructuredAuthenticationConfiguration feature gates, both beta and on-by-default in Kubernetes 1.32 (per pkg/features/versioned_kube_features.go in release-1.32). No --feature-gates flag needed for the version metalk8s pins.
  • Authenticate the salt http.wait_for_successful_query probes that hit /healthz on the apiserver (one direct, six through the local apiserver-proxy :7443 raw stream) using the existing apiserver-kubelet-client.{crt,key} (CN kube-apiserver-kubelet-client, group system:masters). Defence-in-depth: those probes still succeed even though /healthz is now anonymous-allowed, and the cert pattern matches what the etcd state already does.
  • Expose apiserver-kubelet-client.key as certificates.client.files['apiserver-kubelet'].key in salt/metalk8s/defaults.yaml and switch the existing literal references in apiserver/installed.sls and apiserver/certs/kubelet-client.sls to it, to keep the path in a single place.
  • Tests in tests/post/features/authentication.feature:
    • Original advisory regression scenario: anonymous GET /api/kubernetes/version401 Unauthorized. Still passes — /version is not in the allowed anonymous list.
    • New scenario outline: anonymous GET /api/kubernetes/{livez,readyz,healthz} via the control-plane Ingress → 200 ok.
    • New scenario outline: authenticated GET /{livez,readyz,healthz} directly on the API server (admin client cert pulled from the kubeconfig) → 200 ok.

Acceptance criteria:

  • kubectl, salt, kubelet → apiserver, and the bootstrap/post-install flow continue to work end-to-end (single-node and multi-node e2e green).
  • On a deployed cluster:
    • curl -k https://<INSTANCE_IP>:8443/api/kubernetes/version returns 401, body Unauthorized.
    • curl -k https://<INSTANCE_IP>:8443/api/kubernetes/livez (and /readyz, /healthz) returns 200, body ok.
  • CI green, including the three kube-apiserver … scenarios in tests/post/features/authentication.feature.
  • Login via the configured OIDC IDP (Dex by default; Keycloak under ARTESCA) keeps working — verified by Dex BDD scenario and ARTESCA SIT.

Follow-ups:

  • MK8S-258: bump apiVersion from apiserver.config.k8s.io/v1beta1 to v1 once metalk8s pins Kubernetes ≥ 1.34 (no urgency — v1beta1 still served through 1.36+).

See: MK8S-187

g-carre added 2 commits April 30, 2026 18:04
kube-apiserver was running with the default --anonymous-auth=true,
letting unauthenticated callers reach endpoints bound to
system:public-info-viewer (e.g. /version, /healthz) and disclose
Kubernetes/Go versions usable for further attack planning.

Set --anonymous-auth=false so the system:anonymous user cannot
authenticate at all, which neutralises the default kubeadm-managed
ClusterRoleBindings granting access to system:unauthenticated.

Signed-off-by: Guillaume Carre <guillaume.carre@scality.com>
Bake the security advisory's diagnostic into the post-deploy BDD suite
so a regression on --anonymous-auth is caught by CI: hit the advisory's
exact endpoint (/api/kubernetes/version through the control-plane
ingress, which proxies to kube-apiserver) without credentials and
expect a 401 Unauthorized.

Reuses existing 'perform a request on ... on control-plane Ingress' and
'the server returns ... with message ...' steps; no new step code.

Signed-off-by: Guillaume Carre <guillaume.carre@scality.com>
@g-carre g-carre requested a review from a team as a code owner May 4, 2026 06:54
@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 4, 2026

Hello g-carre,

My role is to assist you with the merge of this
pull request. Please type @bert-e help to get information
on this process, or consult the user documentation.

Available options
name description privileged authored
/after_pull_request Wait for the given pull request id to be merged before continuing with the current one.
/bypass_author_approval Bypass the pull request author's approval
/bypass_build_status Bypass the build and test status
/bypass_commit_size Bypass the check on the size of the changeset TBA
/bypass_incompatible_branch Bypass the check on the source branch prefix
/bypass_jira_check Bypass the Jira issue check
/bypass_peer_approval Bypass the pull request peers' approval
/bypass_leader_approval Bypass the pull request leaders' approval
/approve Instruct Bert-E that the author has approved the pull request. ✍️
/create_pull_requests Allow the creation of integration pull requests.
/create_integration_branches Allow the creation of integration branches.
/no_octopus Prevent Wall-E from doing any octopus merge and use multiple consecutive merge instead
/unanimity Change review acceptance criteria from one reviewer at least to all reviewers
/wait Instruct Bert-E not to run until further notice.
Available commands
name description privileged
/help Print Bert-E's manual in the pull request.
/status Print Bert-E's current status in the pull request TBA
/clear Remove all comments from Bert-E from the history TBA
/retry Re-start a fresh build TBA
/build Re-start a fresh build TBA
/force_reset Delete integration branches & pull requests, and restart merge process from the beginning.
/reset Try to remove integration branches unless there are commits on them which do not appear on the source branch.

Status report is not available.

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 4, 2026

Request integration branches

Waiting for integration branch creation to be requested by the user.

To request integration branches, please comment on this pull request with the following command:

/create_integration_branches

Alternatively, the /approve and /create_pull_requests commands will automatically
create the integration branches.

@g-carre g-carre marked this pull request as draft May 4, 2026 06:57
@g-carre
Copy link
Copy Markdown
Contributor Author

g-carre commented May 4, 2026

/create_integration_branches

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 4, 2026

Integration data created

I have created the integration data for the additional destination branches.

The following branches will NOT be impacted:

  • development/123.0
  • development/124.0
  • development/124.1
  • development/125.0
  • development/126.0
  • development/127.0
  • development/128.0
  • development/129.0
  • development/130.0
  • development/131.0
  • development/2.0
  • development/2.1
  • development/2.10
  • development/2.11
  • development/2.2
  • development/2.3
  • development/2.4
  • development/2.5
  • development/2.6
  • development/2.7
  • development/2.8
  • development/2.9

You can set option create_pull_requests if you need me to create
integration pull requests in addition to integration branches, with:

@bert-e create_pull_requests

The following options are set: create_integration_branches

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 4, 2026

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • 2 peers

Peer approvals must include at least 1 approval from the following list:

The following options are set: create_integration_branches

…K8S-187)

Disabling --anonymous-auth in commit f39d88d caused bootstrap to hang
on the salt http.wait_for_successful_query probes that hit /healthz on
kube-apiserver: the unauthenticated GET now returns 401, the state
times out after 5 minutes, and every dependent orchestration step
fails with "One or more requisite failed".

Pass the apiserver-kubelet client cert and key (CN
kube-apiserver-kubelet-client, O system:masters) to all seven affected
probes -- the direct one on https://<host>:6443/healthz in
kubernetes/apiserver/installed.sls, and the six going through the
local apiserver-proxy on https://127.0.0.1:7443/healthz in the
bootstrap, deploy_node, upgrade and downgrade orchestrations. The
salt-master Pod already mounts /etc/kubernetes/pki, and the cert
exists on every master before kube-apiserver starts, so no extra
plumbing is needed.

Expose apiserver-kubelet-client.key as
certificates.client.files['apiserver-kubelet'].key in defaults.yaml
and switch existing literal references in apiserver/installed.sls and
apiserver/certs/kubelet-client.sls to it, to keep the path in a single
place.

Signed-off-by: Guillaume Carre <guillaume.carre@scality.com>
@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 5, 2026

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • 2 peers

Peer approvals must include at least 1 approval from the following list:

The following options are set: create_integration_branches

@g-carre g-carre marked this pull request as ready for review May 5, 2026 15:54
@g-carre
Copy link
Copy Markdown
Contributor Author

g-carre commented May 5, 2026

/reset

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 5, 2026

Reset complete

I have successfully deleted this pull request's integration branches.

The following options are set: create_integration_branches

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 5, 2026

Integration data created

I have created the integration data for the additional destination branches.

The following branches will NOT be impacted:

  • development/123.0
  • development/124.0
  • development/124.1
  • development/125.0
  • development/126.0
  • development/127.0
  • development/128.0
  • development/129.0
  • development/130.0
  • development/131.0
  • development/2.0
  • development/2.1
  • development/2.10
  • development/2.11
  • development/2.2
  • development/2.3
  • development/2.4
  • development/2.5
  • development/2.6
  • development/2.7
  • development/2.8
  • development/2.9

You can set option create_pull_requests if you need me to create
integration pull requests in addition to integration branches, with:

@bert-e create_pull_requests

The following options are set: create_integration_branches

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 5, 2026

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • 2 peers

Peer approvals must include at least 1 approval from the following list:

The following options are set: create_integration_branches

@g-carre g-carre requested a review from Copilot May 5, 2026 20:03
@g-carre g-carre self-assigned this May 5, 2026
@scality scality deleted a comment from bert-e May 5, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the Kubernetes control plane by restricting kube-apiserver anonymous authentication to health endpoints only, while ensuring Salt orchestration health probes and post-install tests continue to function.

Changes:

  • Add a kube-apiserver AuthenticationConfiguration to allow anonymous access only to /livez, /readyz, and /healthz, and wire it into the apiserver static pod manifest.
  • Update Salt http.wait_for_successful_query probes (bootstrap/deploy/upgrade/downgrade and apiserver install readiness checks) to use the existing apiserver-kubelet client certificate.
  • Extend post-install authentication BDD tests to assert /api/kubernetes/version is rejected anonymously while health endpoints remain accessible (anonymous via Ingress; authenticated directly to apiserver).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/post/steps/test_authentication.py Adds new pytest-bdd scenario bindings and a step to perform mTLS-authenticated requests directly against the apiserver.
tests/post/features/authentication.feature Adds scenarios covering anonymous rejection of /version, anonymous allowance of health endpoints via Ingress, and authenticated health endpoint access directly.
salt/metalk8s/orchestrate/upgrade/init.sls Authenticates apiserver availability checks during upgrade using the apiserver-kubelet client cert.
salt/metalk8s/orchestrate/downgrade/init.sls Authenticates apiserver availability checks during downgrade using the apiserver-kubelet client cert.
salt/metalk8s/orchestrate/deploy_node.sls Authenticates apiserver availability checks before/after highstate using the apiserver-kubelet client cert.
salt/metalk8s/orchestrate/bootstrap/pre-upgrade.sls Authenticates the local proxy apiserver availability check prior to master upgrade.
salt/metalk8s/orchestrate/bootstrap/pre-downgrade.sls Authenticates the local proxy apiserver availability check prior to master downgrade.
salt/metalk8s/orchestrate/bootstrap/init.sls Authenticates bootstrap apiserver availability checks using the apiserver-kubelet client cert.
salt/metalk8s/kubernetes/apiserver/installed.sls Mounts and passes the new authentication config into kube-apiserver; switches kubelet client key path to the centralized defaults map; authenticates readiness probe.
salt/metalk8s/kubernetes/apiserver/init.sls Includes the new authnconfig sub-state in the apiserver state bundle.
salt/metalk8s/kubernetes/apiserver/certs/kubelet-client.sls Switches the kubelet-client private key path to the centralized defaults map.
salt/metalk8s/kubernetes/apiserver/authnconfig.sls New state generating /etc/kubernetes/authentication-config.yaml restricting anonymous auth to health endpoints only.
salt/metalk8s/defaults.yaml Adds the apiserver-kubelet client key path into the certificates.client.files map for centralized reuse.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/post/features/authentication.feature
Comment thread tests/post/features/authentication.feature
@g-carre g-carre force-pushed the bugfix/MK8S-187-disable-kube-apiserver-anonymous-auth branch from 61d346a to 15c0da2 Compare May 5, 2026 21:17
@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 5, 2026

History mismatch

Merge commit #1b0256add37d05f04c884aa2f6a8b0f5531c24ed on the integration branch
w/133.0/bugfix/MK8S-187-disable-kube-apiserver-anonymous-auth is merging a branch which is neither the current
branch bugfix/MK8S-187-disable-kube-apiserver-anonymous-auth nor the development branch
development/133.0.

It is likely due to a rebase of the branch bugfix/MK8S-187-disable-kube-apiserver-anonymous-auth and the
merge is not possible until all related w/* branches are deleted or updated.

Please use the reset command to have me reinitialize these branches.

The following options are set: create_integration_branches

@g-carre
Copy link
Copy Markdown
Contributor Author

g-carre commented May 5, 2026

/reset

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 5, 2026

Reset complete

I have successfully deleted this pull request's integration branches.

The following options are set: create_integration_branches

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 5, 2026

Integration data created

I have created the integration data for the additional destination branches.

The following branches will NOT be impacted:

  • development/123.0
  • development/124.0
  • development/124.1
  • development/125.0
  • development/126.0
  • development/127.0
  • development/128.0
  • development/129.0
  • development/130.0
  • development/131.0
  • development/2.0
  • development/2.1
  • development/2.10
  • development/2.11
  • development/2.2
  • development/2.3
  • development/2.4
  • development/2.5
  • development/2.6
  • development/2.7
  • development/2.8
  • development/2.9

You can set option create_pull_requests if you need me to create
integration pull requests in addition to integration branches, with:

@bert-e create_pull_requests

The following options are set: create_integration_branches

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 5, 2026

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • 2 peers

Peer approvals must include at least 1 approval from the following list:

The following options are set: create_integration_branches

@g-carre g-carre force-pushed the bugfix/MK8S-187-disable-kube-apiserver-anonymous-auth branch from 15c0da2 to 10e4ed5 Compare May 6, 2026 09:54
@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 6, 2026

History mismatch

Merge commit #68f2a0394cd43770668080f6bc58eaa70f2ac9b6 on the integration branch
w/133.0/bugfix/MK8S-187-disable-kube-apiserver-anonymous-auth is merging a branch which is neither the current
branch bugfix/MK8S-187-disable-kube-apiserver-anonymous-auth nor the development branch
development/133.0.

It is likely due to a rebase of the branch bugfix/MK8S-187-disable-kube-apiserver-anonymous-auth and the
merge is not possible until all related w/* branches are deleted or updated.

Please use the reset command to have me reinitialize these branches.

The following options are set: create_integration_branches

@g-carre g-carre force-pushed the bugfix/MK8S-187-disable-kube-apiserver-anonymous-auth branch from 10e4ed5 to 6132e5c Compare May 6, 2026 12:53
@g-carre g-carre force-pushed the bugfix/MK8S-187-disable-kube-apiserver-anonymous-auth branch 3 times, most recently from 904e998 to 1cb9435 Compare May 6, 2026 19:25
…8S-187)

Switch kube-apiserver from --anonymous-auth=false to an
AuthenticationConfiguration that allows anonymous access to /livez,
/readyz and /healthz only. This restores kubelet livenessProbe,
startupProbe and readinessProbe (plain httpGet, no way to attach
credentials) while every other endpoint -- /version, /api/*, the
discovery surface -- still requires authentication, so the original
goal of MK8S-187 (neutralising the kubeadm system:public-info-viewer
binding to system:anonymous) is preserved.

The previous --anonymous-auth=false approach broke kubelet probes:
unauthenticated GETs to /livez and /readyz returned 401, and after
startupProbe.failureThreshold * periodSeconds (~250s) kubelet would
have killed and restarted the apiserver in a permanent crash-loop.
The salt http.wait_for_successful_query timeout on /healthz (fixed in
68189f9 by attaching a client cert) was the visible symptom; the
kubelet-probe failure is the underlying one and cannot be fixed with
client certs because httpGet probes do not support TLS client auth.

Generate /etc/kubernetes/authentication-config.yaml from a sibling
salt state (mirroring cryptconfig.sls), mount it into the static pod
as a File volume, and wire --authentication-config to it.
--anonymous-auth is dropped because it is mutually exclusive with the
anonymous block in AuthenticationConfiguration.

Move the OIDC issuer configuration from --oidc-* CLI flags to the
AuthenticationConfiguration `jwt:` array. K8s 1.32 rejects the two
mechanisms together (pkg/kubeapiserver/options/authentication.go:
"authentication-config file and oidc-* flags are mutually
exclusive"); a first apiserver start can succeed before the
control-plane Ingress endpoint is known (no --oidc-* flags emitted),
but the subsequent "Reconfigure control plane Ingress" pass populates
oidc_config in the pillar, re-renders the manifest with both flag
sets, and the apiserver crash-loops on startup. The same five
fields the legacy code consumed (issuerURL, clientID, CAFile,
usernameClaim, groupsClaim) feed the new jwt issuer block. The CA
PEM is read from the salt mine entry `ingress_ca_b64` for the default
Dex / Ingress path (avoiding any state-graph ordering hazard against
the on-disk file), and falls back to salt['file.read'] for a
pillar-supplied OIDC override pointing at a different CA path. When
the username claim is `email`, a `claimValidationRules` CEL
expression `claims.?email_verified.orValue(true)` is added to
reproduce the implicit `email_verified == true` guard that the legacy
--oidc-username-claim=email flag used to apply automatically. ARTESCA
configures Keycloak via the same OIDC pillar override, so its
reconfiguration path is unaffected.

Relies on the AnonymousAuthConfigurableEndpoints and
StructuredAuthenticationConfiguration feature gates, both beta and
on-by-default in Kubernetes 1.32 (per
pkg/features/versioned_kube_features.go in release-1.32) -- no extra
--feature-gates flag needed for the version metalk8s pins.

Extend the post-install authentication feature with two scenario
outlines that lock the contract in place: anonymous access to /livez,
/readyz, /healthz returns 200 'ok' (via the control-plane Ingress,
same path as the existing "rejects anonymous" test), and authenticated
access (admin client cert from the kubeconfig) to the same three paths
also returns 200 'ok'. The pre-existing /version 401 scenario still
passes because /version is not in the allowed anonymous path list.

Signed-off-by: Guillaume Carre <guillaume.carre@scality.com>
@g-carre g-carre force-pushed the bugfix/MK8S-187-disable-kube-apiserver-anonymous-auth branch from 1cb9435 to 250a7d0 Compare May 6, 2026 21:24
@g-carre
Copy link
Copy Markdown
Contributor Author

g-carre commented May 7, 2026

/reset

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 7, 2026

Reset complete

I have successfully deleted this pull request's integration branches.

The following options are set: create_integration_branches

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 7, 2026

Integration data created

I have created the integration data for the additional destination branches.

The following branches will NOT be impacted:

  • development/123.0
  • development/124.0
  • development/124.1
  • development/125.0
  • development/126.0
  • development/127.0
  • development/128.0
  • development/129.0
  • development/130.0
  • development/131.0
  • development/2.0
  • development/2.1
  • development/2.10
  • development/2.11
  • development/2.2
  • development/2.3
  • development/2.4
  • development/2.5
  • development/2.6
  • development/2.7
  • development/2.8
  • development/2.9

You can set option create_pull_requests if you need me to create
integration pull requests in addition to integration branches, with:

@bert-e create_pull_requests

The following options are set: create_integration_branches

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 7, 2026

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • 2 peers

Peer approvals must include at least 1 approval from the following list:

The following options are set: create_integration_branches

@g-carre g-carre requested a review from kg-scality May 7, 2026 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants