Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Release 133.0.6 (in development)

### Bug Fixes

- Disable anonymous authentication on `kube-apiserver`, except for the kubelet
probe endpoints (`livez`, `readyz`, `healthz`)
(PR[#4900](https://github.com/scality/metalk8s/pull/4900))

## Release 133.0.5

### Enhancements
Expand Down
1 change: 1 addition & 0 deletions buildchain/buildchain/salt_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,7 @@ def _download_ui_operator_crds() -> str:
Path("salt/metalk8s/kubernetes/apiserver/certs/server.sls"),
Path("salt/metalk8s/kubernetes/apiserver/init.sls"),
Path("salt/metalk8s/kubernetes/apiserver/installed.sls"),
Path("salt/metalk8s/kubernetes/apiserver/authnconfig.sls"),
Path("salt/metalk8s/kubernetes/apiserver/cryptconfig.sls"),
Path("salt/metalk8s/kubernetes/apiserver/kubeconfig.sls"),
Path("salt/metalk8s/kubernetes/apiserver-proxy/files/apiserver-proxy.conf.j2"),
Expand Down
2 changes: 2 additions & 0 deletions salt/metalk8s/defaults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ kube_api:
cert:
server_signing_policy: kube_apiserver_server_policy
client_signing_policy: kube_apiserver_client_policy
authn_config_path: /etc/kubernetes/authn_config.yaml

etcd:
ca:
Expand Down Expand Up @@ -224,6 +225,7 @@ certificates:
watched: False
apiserver-kubelet:
path: /etc/kubernetes/pki/apiserver-kubelet-client.crt
key: /etc/kubernetes/pki/apiserver-kubelet-client.key
renew:
sls:
- metalk8s.kubernetes.apiserver.certs.kubelet-client
Expand Down
99 changes: 99 additions & 0 deletions salt/metalk8s/kubernetes/apiserver/authnconfig.sls
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Generate the AuthenticationConfiguration file consumed by kube-apiserver
# via --authentication-config.
#
# It carries:
# * `anonymous` -- limits anonymous access to the kubelet probe endpoints
# (/livez, /readyz, /healthz) so kubelet httpGet probes keep working,
# while every other path (/version, /api/*, ...) still requires
# authentication.
# * `jwt` -- the OIDC issuer (Dex by default, or a pillar override). This
# replaces the legacy --oidc-* command-line flags, which are mutually
# exclusive with --authentication-config in Kubernetes 1.32+
# (pkg/kubeapiserver/options/authentication.go: "authentication-config
# file and oidc-* flags are mutually exclusive").
#
# Relies on the AnonymousAuthConfigurableEndpoints and
# StructuredAuthenticationConfiguration feature gates, both beta and
# on-by-default in Kubernetes 1.32+.
{%- from "metalk8s/map.jinja" import kube_api with context %}

include:
- .installed
Comment thread
g-carre marked this conversation as resolved.
- metalk8s.addons.nginx-ingress.ca.advertised

{%- set authn_config_path = kube_api.authn_config_path %}

{#- Build the OIDC issuer config and resolve the matching CA PEM, mirroring the
historical --oidc-* selection logic that used to live in installed.sls.
AuthenticationConfiguration's `jwt[].issuer.certificateAuthority` field
expects PEM content inline (not a file path), so each branch resolves the
CA the way that fits its source:
* pillar override -- read the user-specified CAFile from the salt master.
* default Dex -- pull the Ingress CA from the salt mine
(`ingress_ca_b64`, published by
`metalk8s.addons.nginx-ingress.ca.installed`), which
avoids any ordering dependency on the on-disk file. #}
{%- set oidc_config = {} %}
{%- set ca_pem = '' %}
{%- if pillar.kubernetes.get("apiServer", {}).get("oidc") %}
{%- do oidc_config.update(pillar.kubernetes.apiServer.oidc) %}
{%- if oidc_config.get('CAFile') %}
{%- set ca_pem = salt['file.read'](oidc_config.CAFile) %}
{%- endif %}
{%- elif pillar.addons.dex.enabled and salt.metalk8s_network.get_control_plane_ingress_endpoint() %}
{%- do oidc_config.update({
"issuerURL": salt.metalk8s_network.get_control_plane_ingress_endpoint() ~ "/oidc",
"clientID": "oidc-auth-client",
"CAFile": "/etc/metalk8s/pki/nginx-ingress/ca.crt",
"usernameClaim": "email",
"groupsClaim": "groups",
}) %}
{%- set ingress_ca_mine = salt['mine.get'](pillar.metalk8s.ca.minion, 'ingress_ca_b64') %}
{%- if ingress_ca_mine %}
{%- set ca_pem = salt['hashutil.base64_b64decode'](ingress_ca_mine[pillar.metalk8s.ca.minion]) %}
{%- endif %}
{%- endif %}


Create kube-apiserver authentication configuration:
file.serialize:
- name: {{ authn_config_path }}
- mode: '0600'
- user: root
- group: root
- makedirs: True
- dataset:
{#- TODO(MK8S-258): bump apiVersion to apiserver.config.k8s.io/v1 once metalk8s
pins Kubernetes >= 1.34. AuthenticationConfiguration is registered in
v1beta1 in 1.32/1.33 and promotes to v1 (GA) in 1.34.
it will also continue to be supported in v1beta1 until 1.36 #}
apiVersion: apiserver.config.k8s.io/v1beta1
kind: AuthenticationConfiguration
anonymous:
enabled: true
conditions:
- path: /livez
- path: /readyz
- path: /healthz
{%- if oidc_config and ca_pem %}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When a pillar-override OIDC config omits CAFile or the file is empty, ca_pem stays empty and the entire jwt block is silently skipped — OIDC login breaks with no error. The old --oidc-* flag approach would produce a noisy apiserver startup failure, making the misconfiguration immediately obvious.

Consider adding a guard state that fails explicitly when oidc_config is truthy but ca_pem is not, e.g.:

jinja<br>{%- if oidc_config and not ca_pem %}<br>Fail — OIDC configured but CA certificate is unavailable:<br> test.fail_without_changes:<br> - name: >-<br> OIDC issuer {{ oidc_config.issuerURL }} is configured but its CA<br> certificate could not be read. Check pillar CAFile or the<br> ingress_ca_b64 mine entry.<br>{%- endif %}<br>

— Claude Code

jwt:
- issuer:
url: {{ oidc_config.issuerURL }}
audiences:
- {{ oidc_config.clientID }}
certificateAuthority: {{ ca_pem | tojson }}
claimMappings:
username:
claim: {{ oidc_config.usernameClaim }}
prefix: "oidc:"
groups:
claim: {{ oidc_config.groupsClaim }}
prefix: "oidc:"
{%- if oidc_config.usernameClaim == 'email' %}
claimValidationRules:
- expression: "claims.?email_verified.orValue(true) == true"
message: "email_verified claim must be true when set"
{%- endif %}
{%- endif %}
- require_in:
- metalk8s: Create kube-apiserver Pod manifest
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{%- from "metalk8s/map.jinja" import certificates with context %}
{%- from "metalk8s/map.jinja" import kube_api with context %}

{%- set private_key_path = "/etc/kubernetes/pki/apiserver-kubelet-client.key" %}
{%- set private_key_path = certificates.client.files['apiserver-kubelet'].key %}

include:
- metalk8s.internal.m2crypto
Expand Down
2 changes: 2 additions & 0 deletions salt/metalk8s/kubernetes/apiserver/init.sls
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
# * installed -> deploy apiserver manifest
# * kubeconfig -> create admin kubeconfig file
# * cryptconfig -> create apiserver encryption configuration
# * authnconfig -> create apiserver authentication configuration
#
include:
- .installed
- .kubeconfig
- .cryptconfig
- .authnconfig
36 changes: 12 additions & 24 deletions salt/metalk8s/kubernetes/apiserver/installed.sls
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
{%- from "metalk8s/map.jinja" import coredns with context %}
{%- from "metalk8s/map.jinja" import metalk8s with context %}
{%- from "metalk8s/map.jinja" import networks with context %}
{%- from "metalk8s/map.jinja" import kube_api with context %}

{%- set encryption_k8s_path = "/etc/kubernetes/encryption.conf" %}

{%- set authn_config_path = kube_api.authn_config_path %}
include:
- metalk8s.kubernetes.ca.advertised
- metalk8s.kubernetes.sa.advertised
Expand Down Expand Up @@ -34,18 +35,9 @@ include:
{%- do feature_gates.append(feature ~ "=" ~ value) %}
{%- endfor %}

{%- set oidc_config = {} %}
{%- if pillar.kubernetes.get("apiServer", {}).get("oidc") %}
{%- do oidc_config.update(pillar.kubernetes.apiServer.oidc) %}
{%- elif pillar.addons.dex.enabled and salt.metalk8s_network.get_control_plane_ingress_endpoint() %}
{%- do oidc_config.update({
"issuerURL": salt.metalk8s_network.get_control_plane_ingress_endpoint() ~ "/oidc",
"clientID": "oidc-auth-client",
"CAFile": "/etc/metalk8s/pki/nginx-ingress/ca.crt",
"usernameClaim": "email",
"groupsClaim": "groups",
}) %}
{%- endif %}
{# OIDC is configured via the AuthenticationConfiguration file written by
.authnconfig (--oidc-* flags are mutually exclusive with
--authentication-config in Kubernetes 1.32+). #}

{%- set pod_name = "kube-apiserver-" ~ grains.id %}
{%- set last_pod_id = salt.cri.get_pod_id(
Expand All @@ -58,12 +50,13 @@ Create kube-apiserver Pod manifest:
- source: salt://metalk8s/kubernetes/files/control-plane-manifest.yaml.j2
- config_files:
- {{ encryption_k8s_path }}
- {{ authn_config_path }}
- {{ certificates.server.files.apiserver.path }}
- /etc/kubernetes/pki/apiserver.key
- {{ certificates.client.files['apiserver-etcd'].path }}
- /etc/kubernetes/pki/apiserver-etcd-client.key
- {{ certificates.client.files['apiserver-kubelet'].path }}
- /etc/kubernetes/pki/apiserver-kubelet-client.key
- {{ certificates.client.files['apiserver-kubelet'].key }}
- /etc/kubernetes/pki/ca.crt
- /etc/kubernetes/pki/etcd/ca.crt
- /etc/kubernetes/pki/front-proxy-ca.crt
Expand Down Expand Up @@ -94,7 +87,7 @@ Create kube-apiserver Pod manifest:
- --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
- --etcd-servers={{ etcd_servers | join(",") }}
- --kubelet-client-certificate={{ certificates.client.files['apiserver-kubelet'].path }}
- --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key
- --kubelet-client-key={{ certificates.client.files['apiserver-kubelet'].key }}
- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
- --proxy-client-cert-file={{ certificates.client.files['front-proxy'].path }}
- --proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key
Expand All @@ -114,16 +107,8 @@ Create kube-apiserver Pod manifest:
- --bind-address={{ host }}
- --encryption-provider-config={{ encryption_k8s_path }}
- --cors-allowed-origins=^.*$
{%- if oidc_config %}
- --oidc-issuer-url={{ oidc_config.issuerURL }}
- --oidc-client-id={{ oidc_config.clientID }}
- --oidc-ca-file={{ oidc_config.CAFile }}
- --oidc-username-claim={{ oidc_config.usernameClaim }}
- --oidc-groups-claim={{ oidc_config.groupsClaim }}
- '"--oidc-username-prefix=oidc:"'
- '"--oidc-groups-prefix=oidc:"'
{%- endif %}
- --v={{ 2 if metalk8s.debug else 0 }}
- --authentication-config={{ authn_config_path }}
{% if feature_gates %}
- --feature-gates={{ feature_gates | join(",") }}
{%- endif %}
Expand All @@ -132,6 +117,9 @@ Create kube-apiserver Pod manifest:
- path: {{ encryption_k8s_path }}
type: File
name: k8s-encryption
- path: {{ authn_config_path }}
type: File
name: k8s-authn-config
{%- if grains['os_family'] == 'RedHat' %}
- path: /etc/pki/ca-trust
name: etc-pki-ca-trust
Expand Down
27 changes: 27 additions & 0 deletions tests/post/features/authentication.feature
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,33 @@ Feature: Authentication is up and running
When we perform a request on '/oidc/' on control-plane Ingress
Then the server returns '404' with message '404 page not found'

Scenario: kube-apiserver rejects anonymous requests
Given the Kubernetes API is available
When we perform a request on '/api/kubernetes/version' on control-plane Ingress
Then the server returns '401' with message 'Unauthorized'

Scenario Outline: kube-apiserver allows anonymous access to <path>
Comment thread
g-carre marked this conversation as resolved.
Given the Kubernetes API is available
When we perform an anonymous request on the API server '<path>' endpoint
Then the server returns '200' with message 'ok'

Examples:
| path |
| livez |
| readyz |
| healthz |

Scenario Outline: kube-apiserver accepts authenticated access to <path>
Comment thread
g-carre marked this conversation as resolved.
Given the Kubernetes API is available
When we perform an authenticated request on the API server '<path>' endpoint
Then the server returns '200' with message 'ok'

Examples:
| path |
| livez |
| readyz |
| healthz |

Scenario: Login to Dex using incorrect email
Given the Kubernetes API is available
And the control-plane Ingress path '/oidc' is available
Expand Down
Loading
Loading