[freeradius] release 1.2.0#123
Merged
Merged
Conversation
- Add bundled Bitnami redis subchart (condition: redis.enabled) and the rlm_redis module (modules.redis); host/password auto-wired to the bundled instance via freeradius.redis.* helpers. - Add standalone rlm_cache module (modules.cache) with a driver selector (rbtree/redis/memcached); the redis driver reuses the modules.redis connection. - cert-manager: auto-detect the API and bootstrap a self-signed CA chain (templates/Issuer.yaml) when no issuerRef is given; fix the truthy "apiVersion: false" capability gate; tls.certManager.create now defaults true. - Fix clients.yaml render: skip non-map keys (includeFile/existingConfigMapName) via kindIs instead of a hand-maintained omit list. - Wire image.debug to toggle radiusd -f/-fxx; add a writable /var/run/radiusd emptyDir so the pidfile works under readOnlyRootFilesystem. - Docs: CHANGELOG Unreleased section + README upgrade notes.
- policies.includeDir: include the policy { } block from the image's real
policy.d (relative, resolves under /etc/freeradius) instead of the
non-existent /opt/startechnica/freeradius/policy.d. Fixes the startup error
"Failed reading directory .../policy.d/: No such file or directory" and
restores the bundled default policies (filter_username, eap, ...).
- README: add a "Redis-backed cache" section with the minimum values to enable
the rlm_cache redis driver against the bundled Redis subchart.
ArgoCD runs `helm dependency build`, which falls back to range-resolution when no Chart.lock is present — that queries Docker Hub's tags/list endpoint for oci://registry-1.docker.io/bitnamicharts, now deprecated and returning 404. Stop gitignoring Chart.lock and pin mariadb/postgresql/redis to their exact latest versions so builds pull pinned OCI tags (manifest fetch, no tags/list call).
The cache module rendered `cache { }` with no `update { }` subsection, so
rlm_cache aborted at load: "Must have an 'update' section in order to cache
anything". Render the required section from a new `modules.cache.update`
(default `&reply: += &reply:`), and hard-fail validation when cache is enabled
with an empty update so the misconfig is caught at render instead of crashing
FreeRADIUS at startup.
Override the Bitnami Redis subchart so the cache backend runs as an ephemeral Deployment (master.kind=Deployment, persistence disabled) — appropriate for a non-durable attribute cache.
…workPolicy - Replace the env-vars ConfigMap (templates/configmap/envvars.yaml) with a Secret (templates/secret/envvars.yaml, keeping the <fullname>-envvars name). Drop the unused FREERADIUS_ENABLE_TLS / FREERADIUS_SITES_NAMESPACE keys; the only remaining content is the conditional FREERADIUS_MODS_REST_PASSWORD fallback, now in a Secret instead of a ConfigMap. The Deployment envFrom uses secretRef (the existingConfigmap BYO path still uses configMapRef). - Add NetworkPolicy egress to the Redis backend on modules.redis.port when rlm_redis or the redis-driver cache is enabled.
Expose sites.coa.listen.{type,ipaddr,virtual_server} and render them in
sites/coa.yaml instead of hardcoding the listen address and virtual-server
name. Set modules.cache.driver=redis in values-test.yaml for local testing.
… vars - extract secret-backed container env into freeradius.secretEnvVars helper - gate inner-tunnel files/sql/ldap and wire keycloak REST Auth-Type - add modules.files and modules.ldap toggles - set response format=json on keycloak rest module - document keycloak.scope example
…EAP cert-manager support - move RADSEC/gateway certs into templates/certificates/ alongside new eap-tls cert - issue EAP tls-config server cert via cert-manager when useCertManager is on - rename RADSEC chart-managed Secret default <fullname>-tls -> <fullname>-radsec-tls for parity with eap-tls; BYO Secret names unaffected - gate self-signed eap-tls Secret on (not useCertManager) so it doesn't double-render alongside the cert-manager Certificate
…swords; drop dead sql privkey knob
- rename templates/configmap -> templates/configmaps, templates/secret -> templates/secrets
(matches the certificates/ precedent); fix path references in Deployment.yaml
checksum includes, helper/values comments
- gate sites/EAP private_key_password emission, env var injection, and credentials
Secret key on the corresponding privateKeyPassword value being set; removes
three-way dead weight when no passphrase is configured
- remove modules.sql.tls.privateKeyPassword entirely: rlm_sql drivers
(libmysqlclient tls{}, libpq) don't accept an encrypted leaf key
…lm_cache instances; merge cache configmaps into one mods-enabled/cache file
- sites.tlsCache: new virtual server (cache load/save/clear/refresh, FR 3.2.x convention) backing EAP TLS session resumption via a Redis-backed cache_tls rlm_cache instance; auto-wires modules.eap.tlsConfig.cache when enabled
- keycloak.cache: cache_keycloak rlm_cache instance wired into keycloak_authorize, short-circuits the ROPC + introspection roundtrip on User-Name hits
- modules.cache.instances: user-defined named rlm_cache instances rendered alongside the chart-managed ones
- single mods-cache ConfigMap with one data.cache key holding every "cache <name> { }" block concatenated; Deployment collapses to one volume + mount + checksum
- _validate.tpl: tlsCache / cacheInstances / keycloakCache validators (hard-fail without Redis where required)
…(avoid quoted space)
…ak ConfigMap for module+policy, separate lua-mapper script - keycloak.yaml (new): one ConfigMap rendered for any mode; `keycloak` data key holds the active rlm_lua OR rlm_rest instance (mode-conditional); `keycloak-policy` data key holds the unlang wrapper + role mapping (lua mode only) - keycloak-mapper-lua.yaml (renamed from configmap-lua.yaml): lua-mode-only ConfigMap holding the keycloak-mapper.lua script - configmap-lua.yaml / configmap-rest.yaml / configmap-policy.yaml: removed - Deployment: keycloak.enabled now mounts one `keycloak` volume at mods-enabled/keycloak (always) and policy.d/keycloak (lua only); lua mode adds a `keycloak-mapper-lua` volume at scripts/keycloak-mapper.lua. Two checksums collapse to one (or two in lua mode) - includes the unrelated values.yaml comment tweak (/path/to/openssl -> /usr/bin/openssl)
…olicy.yaml); flip default keycloak.mode to rest
- keycloak-policy.yaml (new): standalone <fullname>-keycloak-policy ConfigMap holding the policy.d/keycloak unlang body (lua mode only). FreeRADIUS only parses policy { } blocks when included from policy.d/, so the policy can't share a ConfigMap+volume with the mods-enabled/ module file
- keycloak.yaml: drop the `keycloak-policy` data key; now renders only the `keycloak` module-config key
- Deployment: separate keycloak-policy volume + checksum (lua mode), mounted at /etc/freeradius/policy.d/keycloak; mods-enabled/keycloak mount unchanged
- values.yaml: default mode changes lua -> rest (REST mode is the simpler path; users opting into Lua role mapping set mode: lua explicitly)
… default-site call to keycloak_authorize for both modes - keycloak-policy.yaml: gate on .keycloak.enabled (was lua-only); mode-conditional body wraps keycloak_lua (lua) or keycloak_rest (rest) with cache-aside; rest branch uses (ok || updated) rcode since rlm_rest returns updated when it parses reply attrs; role mapping (keycloak_roles) stays lua-only — chart's rest module only hits the token endpoint and can't populate role attrs - Deployment: keycloak-policy ConfigMap/volume/mount/checksum now render unconditionally when keycloak.enabled, not just in lua mode - _validate.tpl: drop the "cache.enabled requires mode: lua" rule; the cache wrapper exists for both modes now - sites/default.yaml: collapse the lua/rest branches into a single keycloak_authorize call; the policy handles Auth-Type accept per mode - inner-tunnel.yaml keycloak_rest reference unchanged (rest module as Auth-Type REST backend, separate from authorize-phase policy)
…cy' keyword from inner blocks
FreeRADIUS error at policy.d/keycloak[2]:
Failed to find "policy" as a module or policy.
Files in policy.d/ are $INCLUDEd inside radiusd.conf's outer policy { } block,
so each named policy is declared as bare `<name> { }`. The chart's template
emitted `policy <name> { }`, which the parser treats as a call to a module
named "policy" — fails authorize-section load and refuses to start.
Also restore the (ok) parens in lua mode for consistency with the rest-mode
(ok || updated) rendering.
…ting
The official freeradius/freeradius-server image strips client tools
(radclient/radtest), so running an Access-Request from inside the cluster
requires a separate pod. This adds an opt-in Deployment that:
- runs a small Debian pod (default `debian:bookworm-slim`)
- apt-installs freeradius-utils on first start (cached after that)
- runs `sleep infinity` so it's `kubectl exec`-able
- pre-populates RADIUS_HOST / RADIUS_AUTH_PORT / RADIUS_SECRET env vars
pointing at the chart's own Service, so the test command is short:
radtest <user> <pass> $RADIUS_HOST $RADIUS_NAS_PORT $RADIUS_SECRET
Disabled by default (`radtest.enabled: false`). Intended for short-lived
debugging of Keycloak/EAP/SQL flows — not a production traffic generator.
…ith dropped SETUID/SETGID caps
…n securityContext - image default `debian:bookworm-slim` -> `2stacks/radtest:latest` (Alpine, ~8 MB, ships radtest/radclient preinstalled — no apt-install dance) - drop the apt-get path from the container command; just print a hint and sleep infinity - containerSecurityContext now PSS-restricted by default: runAsUser/Group 65534, runAsNonRoot true, readOnlyRootFilesystem true, drop ALL caps. Safe because radtest only needs a UDP socket + stdin/stdout — no on-disk state, no privileged ops
…o the pod radiusd.conf has `$INCLUDE clients.conf` at top level, and the chart already renders templates/configmaps/clients.yaml from .Values.clients — but Deployment.yaml never mounted the resulting ConfigMap. The container ended up using whatever clients.conf shipped in the image, so any chart-level `clients.<name>` config (including the documented localhost shared secret) silently did nothing. Adds: - volumeMount at /etc/freeradius/clients.conf, subPath clients.conf - volume `freeradius-clients` backed by `<fullname>-clients` (or `.Values.clients.existingConfigMapName` when BYO) - checksum/configmap-clients pod annotation so values changes trigger a rollout
Default `clients.localhost.coa_server: coa` made FreeRADIUS reject the
clients.conf at startup:
/etc/freeradius/clients.conf[1]: No such home_server or home_server_pool "coa"
The `coa_server` directive in a `client { }` block points at a
`home_server` (or `home_server_pool`) used for *outbound* CoA-Request
proxying — NOT at the inbound CoA virtual server. This chart renders
no `home_server` blocks anywhere, so the default value resolved to
nothing.
Inbound CoA (the actual `sites.coa.enabled` feature) is wired through
the `listen { virtual_server = coa }` block in templates/sites/coa.yaml
and is independent of any client.coa_server setting.
- Default to empty string so the template's `if $client.coa_server`
guard skips emitting the directive.
- Doc-clarify what coa_server is actually for, and that the chart does
not yet expose home_server blocks.
… JSON into Session-Timeout / Class; tune rest connection pool; expose require_message_authenticator
- keycloak-policy.yaml rest-mode branch: after a successful keycloak_rest call,
`map json "%{reply:REST-HTTP-Body}"` extracts:
* `/expires_in` -> &reply:Session-Timeout (NAS terminates the session
when the OAuth token would expire, forcing re-auth
against Keycloak)
* `/access_token` -> &reply:Class (opaque pass-through; NAS
round-trips it in accounting for token correlation)
Then strips REST-HTTP-Status-Code / REST-HTTP-Body from the reply so they
don't leak to the NAS. Applies in both cache-enabled (inside the cache-miss
store path) and cache-disabled branches.
- cache_keycloak update {} now also caches &reply:Session-Timeout and
&reply:Class so cache hits in rest mode restore the same attribute set
the fresh path would have produced. Lua mode is unaffected (those reply
attrs are empty there, so caching them is a no-op).
- keycloak.yaml: add pool { start=1 min=1 max=5 spare=1 ttl=3600 } to the
rlm_rest instance — silences the "you probably need to lower min" log spam
when traffic is bursty (default pool was over-provisioned for typical RADIUS
workloads).
- clients.yaml + values.yaml: expose `clients.<name>.require_message_authenticator`,
rendered as `require_message_authenticator = yes` when true. Mitigates the
BlastRADIUS attack (CVE-2024-3596). Default false to preserve back-compat.
Requires `modules.json.enabled: true` for rest mode (the `map json` xlat
comes from rlm_json). FR will fail policy load if json isn't loaded.
…ccess_token (avoids JWT-sized Class attr)
…token-body parsing
Adds `json json_keycloak { }` to mods-enabled/keycloak (rest mode only) and
switches the policy from `map json` to `map json_keycloak`. Self-contains
the Keycloak feature — the rest-mode JSON extraction no longer depends on
the chart-wide `modules.json.enabled` toggle; the dedicated instance loads
rlm_json on its own.
…ycloak rest; suppress status-site auth logs
keycloak rest JSON mapping:
- FR 3.2.x parses `map <name>` against built-in types only; rlm_json's map
proc isn't registered by instance name, so the previous `map json_keycloak`
failed at policy load: "Expecting section start brace '{' after 'map
json_keycloak'". Revert both occurrences to `map json`.
- Drop the now-useless `json json_keycloak {}` block from
mods-config/keycloak/keycloak.yaml (it would have loaded but couldn't be
referenced).
- Add `freeradius.validate.keycloakRest`: hard-fail when keycloak.mode=rest
is set without modules.json.enabled=true, with the exact error message
users would otherwise see at FR startup.
- Fix /access_token vs /session_state inconsistency in the no-cache branch
(the earlier "swap to session_state" only touched the cache branch).
keycloak-policy.yaml mode dispatch:
- Convert remaining implicit `not $isLua` / `else` rest-mode branches to
explicit `$isRest` / `else if $isRest` so adding a third mode later is a
clean addition rather than a silent fall-through.
- Replace the two `ternary "..." "..." $isLua` variable assignments at the
top with an explicit `if $isLua / else if $isRest` block setting $mod
and $okRcode symmetrically.
sites/status.yaml:
- Add per-virtual-server `log { auth = no; auth_badpass = no; auth_goodpass
= no; stripped_names = no }` to silence the "Auth: Login OK" line emitted
on every Status-Server poll from the metrics exporter. Global log config
is unaffected; other virtual servers keep their default verbosity.
- Rename inline status-only client `admin` -> `probe` (cosmetic; clients
match by IP, not by name).
…s no map_proc, map json doesn't parse
FR 3.2.x's rlm_json module is encode-only — there's no map_proc_register
anywhere in the rlm_json source tree (verified rlm_json.c and json.c on
v3.2.x), no jpath xlat, no json_decode. Loading the module enables
%{json_encode:...} (attrs -> JSON string), nothing in the reverse direction.
The `map json` syntax was added in FR 4.0-alpha; using it on 3.2.x produces:
/etc/freeradius/policy.d/keycloak[13]: Expecting section start brace '{'
after "map json"
Errors reading or parsing /etc/freeradius/radiusd.conf
Reverting the JSON-mapping feature accordingly:
- keycloak-policy.yaml: drop the rest-mode `map json ... { Session-Timeout
... Class ... }` blocks from both the cache-miss and no-cache branches,
drop the REST-HTTP-Body/REST-HTTP-Status-Code strip blocks that paired
with them. Rest mode now just calls keycloak_rest and accepts on
(ok || updated), same as the working baseline before the feature.
- modules/cache.yaml: drop &reply:Session-Timeout / &reply:Class from
cache_keycloak's update {}. Caches only &control:<roleAttribute> again
(populated only in lua mode; rest mode still benefits from the ROPC
short-circuit, just with an empty cached attr set).
- _validate.tpl: remove `freeradius.validate.keycloakRest` — no longer
needed since the policy doesn't reference rlm_json. Rest mode works
without modules.json.enabled.
If/when we move to FR 4.x, the mapping can come back in one commit.
… introspection in script modes; rename policies; tighter HTTP-status dispatch
Python backend mode (new):
- keycloak-mapper-python.yaml: rlm_python3 script doing ROPC + JWT-decode +
role extraction (mirrors keycloak-mapper.lua functionally, stdlib-only —
urllib + json + base64 + ssl, no third-party deps)
- keycloak.yaml: `python3 keycloak_python { }` module block (mode-conditional)
- Deployment: subPath mount for /etc/freeradius/scripts/keycloak_mapper.py,
configmap volume, checksum annotation; env vars injected for both
script-driven modes (lua, python). rlm_python3.so is bundled in the
default freeradius/freeradius-server:3.2.8 image — no image rebuild
needed (lua mode still needs a custom image with freeradius-lua).
- keycloak-policy.yaml: $mod/$okRcode dispatch grows a python branch
($okRcode = (ok), matching lua semantics; $hasRoles = isLua OR isPython)
- _validate.tpl: keycloak.mode in {lua, python, rest} hard-fail validator
Drop introspection in lua/python — saves one HTTPS round-trip per auth.
ROPC success already means Keycloak validated the credentials; the access
token's JWT payload (base64url-decoded) carries the role data. The
previous /introspect call added latency without security gain.
Specific HTTP-status handling (lua/python):
None network/TLS error -> FAIL (transient)
400, 401 credentials rejected by Keycloak -> REJECT (final)
200 token issued -> proceed
other unexpected (5xx, misconfig, etc.) -> FAIL
Previously any non-200 collapsed to REJECT, masking operational issues as
auth failures.
`keycloak.roleMapper` (new) — choose role source:
client (default) resource_access[clientId].roles (per-client roles)
realm realm_access.roles (realm-wide roles)
Both script-driven modes honor it via FREERADIUS_KEYCLOAK_ROLE_MAPPER env;
rest mode ignores (rlm_rest doesn't extract roles).
Keycloak TLS — `keycloak.tls.*` (new):
- caCert: inline PEM bundle -> chart renders <fullname>-keycloak-ca Secret
- existingSecret + existingSecretCaKey: BYO Secret name + key (default
ca.crt — standard K8s convention used by cert-manager / kube-root-ca)
- insecure: skip TLS verification (dev only)
All three modes consume the CA consistently:
- rest: rlm_rest tls{} ca_file = ...; check_cert = no when insecure
- python: ssl.create_default_context().load_verify_locations(cafile=...);
ssl._create_unverified_context() when insecure
- lua: per-https.request{} cafile / verify params via TLS_PARAMS
Validator hard-fails when caCert AND existingSecret are both set.
Policy rename for naming-convention consistency (verb_subject):
keycloak_authorize -> authorize_keycloak
keycloak_roles -> roles_keycloak
Call sites in default.yaml and inline comments updated. CHANGELOG
historical entries left as-is (they document the original released names).
CHANGELOG entry for the prior KC_* -> FREERADIUS_KEYCLOAK_* env-var
rename is bundled in this commit (predates the python work but belongs
in the same release).
…Keycloak) The 1.2.0 release notes were written when the dedicated Keycloak module was still the headline feature. Commit 3542d35 then ripped that module out and replaced it with the generic modules.oidc.* module, but the release notes were never updated. Result: CHANGELOG, Chart.yaml's artifacthub.io/changes, and README's "Upgrading -> Unreleased" section all described keycloak.instances.<name>, freeradius.keycloak.* helpers, KC_*/FREERADIUS_KEYCLOAK_* env vars and clients.<x>.keycloak bindings that no longer exist anywhere in the chart. Rewrite all three to reflect what shipped vs. 1.1.0: - ### Added (CHANGELOG + artifacthub): - Headline: generic OIDC module replacing the dedicated Keycloak module. Provider-agnostic (Keycloak/Authentik/Azure AD/Auth0/Okta). - Per-instance feature list (roleMappings/groupMappings, attributeMappings, require, introspect, refreshTokenCache). - Per-instance K8s resources (module/policy ConfigMaps, client-secret + TLS CA Secrets, env wiring). - Shared oidc.py library + per-instance wrappers (single shared <fullname>-oidc-python ConfigMap holds N keys; the consolidation detail from earlier in the cycle is folded into this entry rather than presented as a 1.1.0->1.2.0 "change", since 1.1.0 had no OIDC). - cache oidc[_<name>]_cache integration. - freeradius.oidc.* helpers + freeradius.validate.oidc* validators. - ### Removed (BREAKING — new section in CHANGELOG, mirrored as kind: removed in artifacthub): - Dedicated Keycloak module end-to-end (top-level keycloak.* block, KC_*/FREERADIUS_KEYCLOAK_* env vars, templates/modules/mods-config/keycloak/* ConfigMaps, all freeradius.keycloak.* helpers and validators, clients.<x>.keycloak, Auth-Type REST wiring). - lua mapper-script mode (rlm_lua not bundled in 3.2.8 image). - keycloak.mode: rest (strict subset of python). - ### Changed: dropped the three Keycloak-specific entries (KC_* env var rename, mapper-script filename rename, rest-mode removal) — all three describe an intermediate-state Keycloak that no longer exists. - ### Deprecated: dropped the keycloak.{mode,url,realm,...} top-level deprecation entry — those keys are now removed, not deprecated. - ### Fixed: kept; fixed templates/configmap/clients.yaml path reference to the post-1.1.0 templates/configmaps/clients.yaml. - README "Upgrading -> Unreleased": - Replaced the "Keycloak is now multi-instance" + "Keycloak script env vars renamed" subsections with one "Keycloak module removed; migrate to modules.oidc.*" subsection, including a values before/after diff and a per-instance resource table. - Dropped the "OIDC python wrappers consolidated" subsection — for a 1.1.0 user there's no consolidation to migrate, since 1.1.0 had no OIDC module to begin with. 92/92 helm-unittest tests pass; chart lints clean.
…ectionName on route parentRefs
Adds a single `gateway.proxyProtocol: false` knob that wires PROXY
protocol v1 from the Envoy data plane through to FreeRADIUS in one shot.
When true (and the gateway-api + envoy + tls.enabled preconditions are
met) the chart:
- Renders templates/gateway-api/BackendTrafficPolicy.yaml — an Envoy
Gateway BackendTrafficPolicy (gateway.envoyproxy.io/v1alpha1) scoped
to the FreeRADIUS Service's `tls-radsec` port via
targetRefs[].sectionName, with proxyProtocol.version: V1. Envoy
prepends a PROXY v1 header on every TCP connection to the pod.
- Forces proxy_protocol = yes on the RADSEC listen { } block via an
`or` with the existing sites.radsec.listen.proxy_protocol knob, so
FreeRADIUS sets Packet-Src-IP-Address to the real client IP.
v1 is pinned because FreeRADIUS 3.2.x parses v1 only on its TCP
listeners — v2 is undocumented in the receive path. UDP auth/acct/coa is
unaffected (PROXY protocol is TCP-only and FreeRADIUS UDP sockets can't
parse it regardless). The standalone sites.radsec.listen.proxy_protocol
knob is still there for non-Envoy front-ends (HAProxy, NLB direct-to-pod).
freeradius.validate.gatewayProxyProtocol hard-fails the three
non-functional combinations: gateway.implementation: istio (no BTP
equivalent), gateway.infrastructure: "" (BTP is Envoy Gateway-specific),
and tls.enabled: false (no RADSEC TCP listener to wire into). Caught at
helm install / helm template / helm lint, not at apply.
Side change: chart-rendered UDPRoute (auth/acct/coa) and TLSRoute
(radsec) now emit spec.parentRefs[].sectionName matching the listener
name in the chart's Gateway, so each route attaches to one specific
listener rather than all compatible listeners on the parent. Threaded
through freeradius.gateway.routeParentRefs as a new optional sectionName
argument; routes whose parentRefs are overridden via
gateway.{udpRoute,tlsRoute}.parentRefs are passed through verbatim, and
the ListenerSet-attached branch is unchanged (user controls listener
names there).
Tests added: BTP absent by default, BTP renders with V1 + sectionName
scoping, gateway.proxyProtocol=true flips proxy_protocol=yes on the
RADSEC listen, three validation rejections (tls off / implementation
istio / infrastructure not envoy). Existing UDPRoute and TLSRoute tests
gained spec.parentRefs[0].sectionName assertions. Istio cross-isolation
test updated (BTP replaces CTP; gateway.proxyProtocol removed since the
new validator would reject it under istio). 98/98 pass.
Cleanup: removed a duplicate freeradius.validate.oidcClientBindings
registration in the validation aggregator while in there.
Docs: CHANGELOG (1.2.0 Added + Changed) and artifacthub.io/changes
updated for the new knob, the validator, and the sectionName change.
…; enable EAP-MD5
OIDC dispatch chain was missing from sites/inner-tunnel and rendered
malformed in sites/default whenever a clients.<x>.oidc binding was set:
- Inner-tunnel had zero OIDC wiring despite the 1.2.0 Added-note promising
the dispatch chain into both sites. EAP-TTLS / PEAP tunnelled auth bound
via clients.<x>.oidc fell straight through to pap. Ported the same block
from sites/default, between logintime and pap. Packet-Src-IP[v6]-Address
still reflects the outer NAS inside the tunnel (RFC 5281 §11.2), so the
same NAS-binding logic works unchanged.
- The ipv6 dispatch-arm directive ended in `-}}` which ate the newline +
indent before the `if (...)` action, gluing `if (Packet-Src-IP-Address …) {`
onto the trailing comment line. Result: one big comment line plus an
unmatched closing `}` — radiusd -C would fail. Changed to `}}` (no
forward trim) in both sites. Bug pre-existed in default.yaml since 1.2.0
but only fired with at least one clients.<x>.oidc binding set.
Also enables modules.eap.methods.md5 so the EAP submodule loads, matching
supplicants that request EAP-MD5 as the inner method (note: EAP-MD5 needs
&control:Cleartext-Password — only useful where plaintext password storage
is acceptable).
…e existence
Without the guard, unlang treats the bare-word `Packet-Src-IPv6-Address`
in the OIDC dispatch arm as an xlat (`%{Packet-Src-IPv6-Address}`), and
when the packet arrived over IPv4 the absent attribute expanded to `""`,
which the IPv6 cast then failed to resolve:
EXPAND Packet-Src-IPv6-Address
-->
ERROR: Failed casting lhs operand: Failed resolving "" to IPv6 address
ERROR: Failed retrieving values required to evaluate condition
The cast error stamped Module-Failure-Message and the OR-arm silently
evaluated false, so dispatch missed the matching instance and fell
through to pap. Symmetric risk for IPv6-only requests on the IPv4 leg.
Fix: render each comparison as `(&Attr && Attr == "<addr>")` — `&Attr`
is the attribute-reference form (no xlat) and the existence check short-
circuits the comparison when the attribute isn't present. Applied to
both sites/default and sites/inner-tunnel.
… as a CIDR prefix, not an exact string
The dispatch arm rendered `Packet-Src-IP-Address == "<addr>"`, which is
exact-string equality in unlang. Any CIDR value silently missed:
if ((&Packet-Src-IP-Address && Packet-Src-IP-Address == "0.0.0.0/0")) {
EXPAND Packet-Src-IP-Address
--> 172.18.135.80
if ((&Packet-Src-IP-Address && Packet-Src-IP-Address == "0.0.0.0/0")) -> FALSE
The semantic mismatch was double-sided: the underlying `clients{}` block
has always accepted CIDR for shared-secret matching (it parses prefixes
natively), so anyone wiring an OIDC instance via clients.<x>.oidc with
the same CIDR they used for ipaddr hit a silent dispatch miss and fell
through to pap -> "No Auth-Type found".
Switch the operator to `<=`, which FR3 unlang(5) §CONDITIONS documents
as "checking that an IP address is contained within a network". Single
hosts (`172.18.0.1`) are treated as /32 prefixes containing only
themselves, so the same render works for both CIDR and bare hosts —
no template branching needed. Existence guard (`&Attr && ...`) is kept
so absent attributes still short-circuit cleanly.
Applied to both sites/default and sites/inner-tunnel.
…bute existence
When the IdP returned no roles at the configured `rolesClaim` (or no
groups at `groupsClaim`), the policy hit:
policy oidc_keycloak_roles {
if (&control:Class[*] == "basecamp-wifi") {
ERROR: Failed retrieving values required to evaluate condition
} # policy oidc_keycloak_roles = noop
The `[*]` iterator on an absent attribute is a hard error in unlang, not
a no-match. The rest of the mapping chain is then skipped — auth still
completes (Auth-Type := Accept was already set by oidc_<name>_authorize)
but every "user has no role/group" case stamps a noisy error.
Add the same `&Attr && ...` existence guard the dispatch arms use:
if (&control:Class && &control:Class[*] == "basecamp-wifi") { ... }
Symmetric fix for the groups policy. Same pattern as the IP-cast and the
IP-prefix containment fixes — `[*]` on a missing attribute is the third
"missing-attribute footgun" we've shipped in 1.2.0; the dispatch already
demonstrates the right pattern.
Two themes, no contract change for runtime callers.
Diagnostics — opaque failures become actionable log lines:
- _post_form returns (status, body, err) where err is repr(URLError|
OSError) on network/TLS failure. authorize() and validate() surface
it into radlog instead of swallowing — DNS, TCP, TLS, timeout each
produce a distinct log line. Same treatment for introspect.
- L_DBG around each HTTP call: `ROPC POST <url> (user=...)` /
`ROPC -> HTTP <status>`, `introspect POST <url>`, `refresh_token
POST <url>` / `refresh_token -> HTTP <status>`. Visible under
`radiusd -X` — confirm the rendered tokenUrl/introspectUrl without
exec-into-pod + curl.
- L_AUTH: HTTP body on ROPC 4xx/5xx (RFC 6749 error body —
`{"error":"invalid_grant","error_description":"..."}`) so the
IdP-side rejection reason is visible.
- L_DBG after JWT decode / introspect: sorted top-level claim keys,
plus the RESOLVED value at each configured claim path (required[],
rolesClaim, groupsClaim). Top-level values not logged — keys only,
since values may carry PII.
- "no roles" warning walks the configured rolesClaim path one segment
at a time and names the missing one, listing siblings at the parent
level. Keycloak's `realm_access.roles` vs
`resource_access.<client>.roles` confusion now points at the first
missing segment instead of "no roles at <full path>".
- L_WARN when local JWT decode returns no claims (malformed/truncated
token) — previously every downstream check silently saw an empty
dict.
- L_DBG extracted role/group lists logged as one line each.
- L_DBG token-response extras (Keycloak's `not-before-policy`,
Authentik's `id_token_expires`, Azure's `ext_expires_in`, …) and
`session_state` when present — without baking provider knowledge.
id_token decode (OIDC Core §3.1.3.3):
- The token endpoint returns id_token alongside access_token whenever
the request carries the `openid` scope. id_token is the
authoritative identity bearer (sub, email, name, preferred_username,
…) — claims access_token typically lacks.
- The module now decodes both and uses the union for required[],
rolesClaim, groupsClaim, attributeMappings. id_token wins on
conflicts for shared registered claims; access-token-only fields
(realm_access.roles, resource_access.*) are untouched. Pure OAuth
2.0 providers without id_token degrade silently. Introspection path
unchanged — RFC 7662 stays the single source of truth.
- L_DBG: id_token-only vs access-token-only key sets so you can see
which bearer carried which claim path.
…exporter-secret + 64-char status/exporter secrets
Three independent changes bundled into one release-prep commit.
1. realmPool — auto-generated peer home_server pool for StatefulSet
deployments.
New opt-in top-level key `realmPool.{enabled,name,type,port,proto,secret,
virtualServer}`. When kind=StatefulSet AND replicaCount>1 AND
realmPool.enabled=true, the chart renders one home_server entry per
replica pointing at <fullname>-<ord>.<headless-svc>.<ns>.svc.cluster.
local — the stable per-pod DNS the headless Service already provides —
plus a wrapping home_server_pool listing all of them.
Additive: rendered AFTER user-supplied .Values.homeServers[] /
homeServerPools[], and any same-named user entry wins (mirrors the
existing radsec chart-managed-with-override pattern). Defaults:
type=load-balance, proto=udp, port falls back to containerPorts.auth
(1812). Secret is required and must match the clients{} block that
accepts the peer-pod source IPs (typically the existing
clients.localhost wildcard with ipv4addr: 0.0.0.0/0 already covers it).
New validator freeradius.validate.realmPool hard-fails at helm template
time when:
- realmPool.enabled=true with kind!=StatefulSet (per-pod DNS unavailable)
- realmPool.enabled=true with replicaCount<2 (single-member pool is a no-op)
- realmPool.enabled=true with empty realmPool.secret
Also rides along: new freeradius.utils.joinOrDefault helper that joins
list-shaped cipher_list values into the colon-separated scalar
FreeRADIUS expects, replacing the previous `default` form in
home-servers.yaml + sites/radsec.yaml which would mis-render a list.
2. status virtual server — accept the metrics exporter Deployment.
Previously sites/status had only `client probe { ipaddr = 127.0.0.1 }`,
which (when present) makes FreeRADIUS ignore global clients.conf
entries for this virtual server — so the metrics exporter (different
pod, different IP) was silently rejected at the listener. Add a
`client exporter { ipaddr = 0.0.0.0/0 }` block gated on
metrics.enabled; NetworkPolicy already restricts the status port to
pods labelled component=metrics (templates/NetworkPolicy.yaml), and
the shared secret is the second layer.
3. exporter-secret — dedicated secret end-to-end, separate from sites-status-secret.
- New metrics.secret values knob, auto-generated into the chart
credentials Secret under key `exporter-secret` when empty.
- sites/status `client exporter` uses $ENV{FREERADIUS_EXPORTER_SECRET};
_env.tpl emits the env sourced from `exporter-secret` (gated on
metrics.enabled), with the existingSecretPerPassword.metricsSecret
BYO path honoured.
- metrics/Deployment.yaml RADIUS_PASSWORD switched from
sites-status-secret to exporter-secret, same BYO path honoured.
- Independent rotation, independent blast radius: probe (radclient
loopback) and exporter (metrics pod) now authenticate against
separate Status-Server credentials.
- Bumped both sites-status-secret AND exporter-secret auto-gen length
to 64 chars (previously 10). Existing installs preserve their old
values via the `lookup` path; only fresh installs get the longer
default.
CHANGELOG entries under 1.2.0 §Added and §Changed.
…h probes invalid
K8s rejected the metrics Deployment with
Deployment.apps "freeradius-metrics" is invalid:
spec.template.spec.containers[0].livenessProbe.httpGet:
Forbidden: may not specify more than 1 handler type
spec.template.spec.containers[0].readinessProbe.httpGet:
Forbidden: may not specify more than 1 handler type
Cause: values.yaml `metrics.livenessProbe` / `metrics.readinessProbe`
each carried an `exec: { command: [sh, /health/healthcheck.sh] }`
block AND the chart template appended its own `httpGet: { path:
/metrics, port: metrics }` underneath. Rendered probes had two
handler types — invalid per the K8s probe schema.
The exec config was dead — `/health/healthcheck.sh` doesn't exist in
`bvantagelimited/freeradius_exporter:1.4.4` and isn't mounted from
anywhere, so even if the probe had accepted it, it would have failed
every check. The httpGet against `/metrics:metrics` is the real
working probe (the exporter serves Prometheus metrics on that port
and path by default).
Drop the exec blocks from both probe defaults. Timing fields
(initialDelaySeconds, periodSeconds, …) and `enabled: true` retained;
the template's `httpGet` becomes the sole handler.
… StatefulSet
Add DaemonSet to the kind switch in Application.yaml alongside Deployment
and StatefulSet, plus the related downstream gates.
- Application.yaml: $kindMap accepts `daemonset` -> `DaemonSet`; apiVersion
routes through the existing st-common.capabilities.daemonset.apiVersion
helper. For DaemonSet pods: skip `replicas`, `serviceName`,
`podManagementPolicy` (none of them have semantics for a per-node
workload); emit `updateStrategy:` (DaemonSet shares the field name with
StatefulSet — rollingUpdate vs OnDelete — distinct from Deployment's
`strategy:`). The StatefulSet-only volumeClaimTemplates path is
untouched; DaemonSet falls back to the standalone PVC, which means RWX
storage if you want to share across node pods (or BYO existingClaim /
set persistence.enabled=false for an emptyDir).
- HorizontalPodAutoscaler.yaml: HPA targets workloads that expose the
`scale` subresource — Deployment and StatefulSet qualify, DaemonSet
does not (one pod per node is the whole point). When kind=DaemonSet,
skip the HPA render entirely instead of emitting an HPA that targets a
non-existent scale endpoint. Also fix a latent pre-existing bug: the
HPA's scaleTargetRef.{apiVersion,kind} was hardcoded to Deployment
even when the actual workload was a StatefulSet — anyone running
kind=StatefulSet with the HPA enabled had a non-functional HPA. Now
the scaleTargetRef tracks the rendered kind.
- helpers/_validate.tpl: realmPool validator message updated to mention
both Deployment and DaemonSet as kinds without per-pod stable DNS
(was previously only naming Deployment). Functional behavior unchanged
— realmPool.enabled=true still hard-fails on anything other than
StatefulSet.
- values.yaml: `@param kind` docstring extended to cover DaemonSet
semantics — replicaCount ignored, HPA silently skipped, shared PVC
requires RWX or BYO, realmPool.enabled still requires StatefulSet.
Service-headless.yaml and PersistentVolumeClaim.yaml needed no changes
— their existing `eq ... statefulset` / `ne ... statefulset` gates
already produce the correct behavior for DaemonSet (no headless Service,
shared PVC rendered).
…act OIDC helpers
Three changes bundled.
1. Per-pod Services for StatefulSet (templates/Service-perPod.yaml, new).
When kind: StatefulSet, render one Service per replica named
`<fullname>-<ord>` alongside the main load-balanced Service and the
headless Service. Each per-pod Service targets exactly one pod via
the `statefulset.kubernetes.io/pod-name` label (kube-controller-
manager adds it to every StatefulSet pod automatically). All knobs
inherit from .Values.service.* — no new values keys. Two intentional
differences from the main Service:
- spec.externalTrafficPolicy is hardcoded to Local. Each per-pod
Service has a single backing pod, so the cross-node traffic drop
that Local causes is exactly correct, and the client source IP is
preserved end-to-end (no kube-proxy SNAT). NAS IPs land in
Packet-Src-IP-Address for clients.conf matching.
- spec.ports[].nodePort is unset on all per-pod Services. K8s auto-
allocates a unique NodePort per pod — the main Service keeps the
fixed service.nodePorts.* knob; per-pod NodePorts would otherwise
collide cluster-wide.
Gated purely on kind: StatefulSet. No render under Deployment or
DaemonSet (their pods don't carry the pod-name label).
2. Gateway API routes fan out per-pod for StatefulSet.
When kind: StatefulSet AND gateway.implementation: gateway-api,
each chart-managed route's backendRefs now lists one entry per
replica targeting that replica's per-pod Service (equal weight).
UDPRoute auth/acct/coa and TLSRoute radsec all fan out symmetrically.
Deployment and DaemonSet keep a single backendRef pointing at the
main load-balanced Service. Combined with per-pod
externalTrafficPolicy: Local, this preserves the NAS source IP
end-to-end through the Envoy data plane.
3. Helpers refactor.
- New freeradius.service.portsList in _helpers.tpl: shared port-list
emitter used by both Service.yaml and Service-perPod.yaml. Pass
`suppressNodePorts: true` from the per-pod path to force nodePort:
null regardless of service.type / service.nodePorts.*. Single
source of truth for the port list; drift on port additions is now
impossible. Service.yaml goes from ~60 inline lines to one
`include`.
- New freeradius.gateway.backendRefs in _helpers.tpl: centralises
the Deployment-vs-StatefulSet fan-out logic so future routes pick
it up automatically.
- Extract all 20 freeradius.oidc.* helpers from _helpers.tpl into
a dedicated _oidc.tpl. Pure relocation (no behavioural change):
resolveInstances, envVarPrefix, moduleName / validateModuleName,
policyName / rolesPolicyName / groupsPolicyName, cacheName,
cacheKey, modKey / policyKey / scriptKey, clientSecretName,
tlsVolumeName, tls.{enabled, createSecret, secretName, caKey,
caFilePath}, dispatchArms. _helpers.tpl shrinks from ~671 to ~475
lines; the OIDC surface now lives next to its usage in
templates/modules/oidc/.
Test fixtures: tests/Application_test.yaml — the "rejects an invalid
workload kind" case used `kind: DaemonSet` as the invalid value, but
DaemonSet is now a valid kind (commit 0c5b8ce). Switched to `kind: Job`
as the invalid placeholder and updated the expected error message to
mention DaemonSet.
All 98 helm-unittest cases pass.
…ase names; EAP-TTLS supplicant notes; proxy.conf tabs->spaces Three changes bundled. 1. realmPool auth/acct split (same-release breaking shape change). Previously: when kind=StatefulSet AND realmPool.enabled, the chart emitted one `home_server <name>-<ord>` (type=auth) per replica plus one `home_server_pool <name>` listing them. Now: TWO `home_server` entries per replica — `<name>_<ord>_auth` (type=auth, port=containerPorts.auth) and `<name>_<ord>_acct` (type=acct, port=containerPorts.acct) — plus TWO matching pools `<name>_auth` and `<name>_acct`. Both home_servers per ordinal target the same per-pod DNS name; only type and port differ. Naming switched to snake_case so the rendered identifiers parse cleanly in FreeRADIUS proxy.conf (the upstream convention). Any hyphens in realmPool.name are auto-converted to underscores via a new `freeradius.utils.snakeCase` helper — so user-supplied "my-cluster" still renders to legal "my_cluster_0_auth" / "my_cluster_auth" identifiers. Pool type stays as realmPool.type (default load-balance). Per-entry conflict avoidance still applies — user-supplied homeServers / pools with matching names win and skip the auto-gen for that name. This is a same-release shape change (realmPool only shipped in this 1.2.0 release); anyone already configured needs to update their realm references from `<name>` to `<name>_auth` / `<name>_acct`. Per the established rule, no README §Upgrading entry is added for in-release breaking renames. 2. EAP-TTLS / PEAP supplicant notes in README. New `### EAP-TTLS / PEAP supplicant notes` subsection between the Keycloak/OIDC and Gateway routing sections. Explains the "Outer and inner identities are the same. User privacy is compromised." warning, the recommended supplicant config (Identity / Anonymous identity / Password), the realm-routing caveat, and per-OS rollout paths (Android, iOS/macOS, Windows, MDM). Chart can't fix this — it's a supplicant-side setting on the end-user device. 3. files/proxy.conf: convert tabs to 4 spaces. The bundled proxy.conf (extracted from FreeRADIUS 3.2.8 upstream) used tab indentation. Converted to 4 spaces for visual consistency with the rest of the chart's config files. 655 lines re-indented; semantics unchanged. Future re-extracts from upstream will show as a big diff — the chart's copy is intentionally space-indented; run `expand -t 4` when bumping the bundled version. All 98 helm-unittest cases pass.
… checks across templates Centralises the case-insensitive `kind=StatefulSet` check that was duplicated across 7 call sites (each rendering its own `lower (toString (.Values.kind | default "Deployment"))` expression and local $kind/$rpKind variables). The helper returns the literal string "true" when StatefulSet — truthy in template `if` — and the empty string otherwise; negate with `not (include ...)` for the "render only on Deployment/DaemonSet" gates. Call sites updated: - templates/Service-headless.yaml - templates/Service-perPod.yaml (dropped now-unused $kind local) - templates/PersistentVolumeClaim.yaml - templates/configmaps/home-server-pools.yaml (dropped $rpKind local) - templates/configmaps/home-servers.yaml (dropped $rpKind local) - templates/helpers/_helpers.tpl::gateway.backendRefs - templates/helpers/_validate.tpl::validate.realmPool Pure refactor — no behavioural change. All 98 helm-unittest cases pass unchanged.
…US_REALMPOOL_SECRET}; declare redis.metrics.extraArgs map shape
credentials.yaml synthesizes a 32-char `realmpool-secret` via st-common.secrets.passwords.manage when realmPool.enabled, honouring a user-supplied realmPool.secret as the providedValues override; _env.tpl wires FREERADIUS_REALMPOOL_SECRET from that Secret into the pod; home-servers.yaml swaps the per-pod auto-gen home_server entries from a literal `secret = "<value>"` to `secret = $ENV{FREERADIUS_REALMPOOL_SECRET}` (FreeRADIUS expands at runtime). The matching clients{} entry uses the same env-var indirection. validate.realmPool drops the empty-secret hard-fail (auto-gen handles it) but keeps the kind:StatefulSet + replicaCount>=2 checks. values.yaml redis.metrics.extraArgs: {} re-states the Bitnami subchart's map shape so a list-typed override is an obvious contract violation rather than a coalesce.go warning surfacing only at template time.
Same-release rip-out of the realmPool auto-gen primitive added across this session. Deleted: values.yaml realmPool block + §Auto-gen narrative + the kind: @param sentence + realmpool-secret/realmpoolSecret mentions in auth docstrings; values.schema.json realmPool property; the $rpEnabled-gated auto-gen sections in home-servers.yaml / home-server-pools.yaml; freeradius.validate.realmPool function + its dispatch line in _validate.tpl; FREERADIUS_REALMPOOL_SECRET env entry in _env.tpl; realmpool-secret row in credentials.yaml; freeradius.utils.snakeCase helper (realmPool was its only consumer). freeradius.headlessServiceName + freeradius.isStatefulSet kept — both still used by Service-headless.yaml / Application.yaml / other validators. Validator removed too, so old values files setting realmPool.enabled: true now silently render without it instead of failing loudly.
…oved in 8263dbd) The realmPool §Added bullet + freeradius.utils.snakeCase §Added bullet + freeradius.validate.realmPool §Added bullet all referenced a feature that no longer exists in the 1.2.0 release surface. Clean cancellation: 1.2.0 ships as if realmPool was never added in the first place.
…ol} toggles for StatefulSet peer mesh
Three independent opt-in toggles, each emits one chart-managed config piece named after <fullname> snake-cased: realm DEFAULT { auth_pool = <fullname>_auth_pool; acct_pool = <fullname>_acct_pool }; one home_server <fullname>_<ord>_<auth|acct> per StatefulSet replica with ipaddr pointing at the per-pod headless DNS, secret rendered as literal $ENV{FREERADIUS_DEFAULT_INSTANCE_SECRET}; and two home_server_pool <fullname>_<auth|acct>_pool blocks load-balancing across the per-pod entries. All three silently skip on non-StatefulSet kinds. No cross-toggle validation — enabling one without the others dangles refs and FreeRADIUS fails loudly at startup. No conflict-detection vs the user-facing homeServers[] / homeServerPools[] / realms[] arrays — the createDefaultInstance block is rendered AFTER those loops in its own self-contained section per template (home-servers.yaml, home-server-pools.yaml, realms.yaml). User wires FREERADIUS_DEFAULT_INSTANCE_SECRET via extraEnvVarsSecret and uses the same $ENV{...} expansion in the matching clients{} entry — chart stays out of the credentials path. freeradius.utils.snakeCase helper re-added (one-liner, replace - with _) as the only sole consumer.
… headless DNS Pivot the rendered ipaddr from <pod>-<ord>.<headless-svc>.<ns>.svc.cluster.local (StatefulSet headless A-record) to <pod>-<ord>.<ns>.svc (per-pod ClusterIP Service rendered by Service-perPod.yaml for kind: StatefulSet). The cluster-domain suffix (.cluster.local etc.) is appended by the resolver search path so the short form stays portable. Drops the $cdiHeadless let-binding.
…log{} as logging.*
Chart-managed radiusd.conf body moves out of .Values.configurations (89-line inline string) and out of files/radiusd.conf (1214-line upstream copy with wrong paths) and into templates/configmaps/configuration.yaml itself — single source of truth. .Values.configurations stays as an inline-string escape hatch (user-supplied verbatim radiusd.conf body, evaluated through tplvalues.render); .Values.configurationsConfigMap stays as the BYO ConfigMap escape hatch; the chart-managed body fires only when neither is set. Application.yaml mount + volume gates dropped (always present now). New logging.* block (destination, colourise, file, syslog_facility, stripped_names, auth, auth_badpass, auth_goodpass, msg_denied) surfaces the log{} body as values knobs — booleans render via ternary "yes" "no" to match FreeRADIUS-native truthy syntax; msg_denied gets | quote. Defaults render byte-identical to the prior inline body. Conditional $INCLUDE lines for home-servers.conf / home-server-pools.conf now mirror the createDefaultInstance gates so the include only fires when the underlying configmap renders.
RADSEC is TCP+TLS, not HTTP — %REQ(:AUTHORITY)%, %REQ(X-ENVOY-ORIGINAL-METHOD)%, %REQ(X-ENVOY-ORIGINAL-PATH)%, %REQ(X-PROXY-USER)%, %REQ(REMOTE-USER)%, %REQ(X-REQUEST-ID)%, %RESPONSE_CODE%, and %RESPONSE_FLAGS% all evaluate to empty over a raw TCP listener, plus the basic_auth_user commented placeholder. Keep only transport-layer fields that mean something on TCP: bytes_received / bytes_sent / client_ip / duration / start_time / protocol / upstream_host.
StatefulSet pods now start in parallel instead of OrderedReady. Faster rollouts and faster initial install; the FreeRADIUS workload doesn't depend on per-pod startup ordering (each pod is independent — peer-mesh state is built at the proxy layer via the realms/pools, not via ordered pod readiness).
…ield; breaks sync on existing StatefulSets) The b45af1a change made every existing release fail ArgoCD sync — Kubernetes forbids updates to podManagementPolicy on a live StatefulSet (it's in the same immutable-spec class as serviceName / selector / volumeClaimTemplates). Revert the default to "" (empty → k8s applies its built-in OrderedReady) so existing installs keep working untouched. New installs that want Parallel start opt in explicitly. Docstring now calls out the immutability + the kubectl delete sts --cascade=orphan recovery recipe.
…t; strip vendor OIDC label
Multiple intra-release cleanups bundled:
- logging.destination default flips files -> stdout, and Application.yaml drops the `-l stdout` CLI override that was forcing stdout regardless of the log{} block. The configmap is now the single source of truth for log destination — set logging.destination: files (+ logging.file: <path>) to write to disk, leave at stdout to keep kubectl-logs-native behavior. Pair: chart-side log control finally lives in one place.
- podManagementPolicy default flips back to Parallel after the 0451299 revert. Fresh installs get parallel pod start; the immutability caveat is documented in the @param. Operators upgrading a live release that took the empty default need `kubectl delete sts --cascade=orphan` then resync — same recipe noted in the values.yaml docstring.
- values-test.yaml persistence.enabled flips true -> false so the test overlay runs on ephemeral storage.
- app.firmansyah.id/oidc-instance label stripped from the four OIDC resources (oidc.yaml, oidc-policy.yaml, secret.yaml, secret-tls.yaml). Pure metadata, no in-chart consumer.
…ams (logging.*, createDefaultInstance.*, etc.)
The 1.2.0 release removed the dedicated Keycloak module end-to-end (replaced by modules.oidc.*), but the README still carried the old Keycloak narrative + parameter table + env-var rows + breaking-changes bullets in places that weren't part of the §Upgrading migration doc. Rewrite each obsolete location with the OIDC equivalent: §"OIDC authentication" narrative (was §"Keycloak (OIDC) authentication" — now configured via modules.oidc.instances.<name>, with rolesClaim required, no mode knob, rlm_python3-only); §Parameters "OIDC integration parameters" table (was "Keycloak integration parameters" — 12 keycloak.* rows replaced with modules.oidc.* schema reference); §Chart-managed environment variables table rows + bullets (FREERADIUS_KEYCLOAK_* -> FREERADIUS_OIDC_*); §Breaking Changes 1.2.0 four Keycloak bullets collapsed into one "module removed -> migrate" bullet linking to existing §Upgrading entry. §TODO swept of dead refs (keycloak in sibling-modules list, keycloak.instances[].existingSecret example), with the "OIDC introspect decoupled from Keycloak" item dropped (shipped in 1.2.0). §Parameters typo `configuration` -> `configurations` + description rewritten to match actual escape-hatch behavior; `tls.autoGenerated` row marked DEPRECATED; dead `tls.autoGenerator.certmanager.enabled` row removed.
Same pass also adds missing parameter rows that were never documented: architecture; logging.{destination,colourise,file,syslog_facility,stripped_names,auth,auth_badpass,auth_goodpass,msg_denied}; extraStartupArgs; kind; podManagementPolicy (with the kubectl delete sts --cascade=orphan recovery recipe inline); createDefaultInstance.{realm,homeServer,homeServerPool} in §Proxy/realm.
…ce for DaemonSet too isDaemonSet mirrors isStatefulSet (case-insensitive kind match, returns "true" / empty). Service-headless.yaml gate changes from `if isStatefulSet` to `if or isStatefulSet isDaemonSet` — DaemonSet pods benefit from the same DNS-based pod discovery (one A record per pod IP) that StatefulSet uses; StatefulSet additionally needs it for `serviceName:` per-pod stable DNS. Deployment still skips (workload-wide load-balanced ClusterIP suffices).
…alues.merge call
Application.yaml + Service.yaml metadata.annotations rendering went `{{- if or A B }} annotations: {{- if A }}…{{- end }} {{- if B }}…{{- end }} {{- end }}` — the inner conditional was redundant since the outer `if or` already gated non-empty inputs. Replace with `tplvalues.merge` over the source list + a single `tplvalues.render`. Same output, less template noise; matches the pattern already used in Service-headless.yaml.
…, dead-key removal, docs
Code:
- Snake_case rename batch (BREAKING; same-release silent-ignore): modules.sql.{readGroups, readProfiles} -> read_groups / read_profiles; modules.json.encode.value.{singleValueAsArray, enumAsInteger, datesAsInteger, alwaysString} -> snake_case; modules.cache.maxEntries (and per-instance modules.cache.instances.<name>.maxEntries) -> max_entries; modules.eap.{timerExpire, ignoreUnknownEapTypes, ciscoAccountingUsernameBug, maxSessions, defaultType} -> timer_expire / ignore_unknown_eap_types / cisco_accounting_username_bug / max_sessions / default_eap_type. Validator + error-message text updated alongside the default_eap_type rename. values.schema.json reflects the new shape.
- modules.eap.tlsConfig.cipher_list: string -> []string. Chart joins entries with ":" via freeradius.utils.joinOrDefault; default ["DEFAULT"]; empty list and nil both fall through to DEFAULT. Mirrors the same change already shipped for sites.radsec.tls.cipher_list. Schema rejects the old scalar string form.
- Drop the top-level .Values.architecture key: never referenced anywhere in the chart (the only `architecture` hits in templates/ are .Values.mariadb.architecture and .Values.postgresql.architecture, Bitnami subchart knobs). Removed from values.yaml + values.schema.json + README parameters table.
- sites/default.yaml: cosmetic comment-block indent cleanup + new sites.default.authorize hook for inline rendering (was the bundled-in-tree edit prior to this commit).
Tests:
- tests/schema_test.yaml: +7 new cases — sites.radsec.tls.cipher_list and modules.eap.tlsConfig.cipher_list array-shape positive + scalar-string negative; modules.eap.ignore_unknown_eap_types bool guard; modules.sql.read_groups bool guard; modules.cache.max_entries integer guard.
- tests/NetworkPolicy_test.yaml: first "narrows egress to DNS only" test now pins modules.redis.enabled: false and modules.cache.driver: rbtree explicitly — preserves the test intent under the new module-enabled defaults (cache.driver default flipped to redis upstream, which would otherwise add a second egress rule).
Docs:
- Chart.yaml artifacthub.io/changes: +14 entries covering createDefaultInstance, logging.*, isDaemonSet helper + headless Service for DaemonSet, radiusd.conf body move, log destination via configmap, podManagementPolicy default flip, snake_case batch, cipher_list array shape on both surfaces, EnvoyProxy access-log trim, firmansyah.id label strip, annotation rendering refactor, redis.metrics.extraArgs defensive shape; stale "defaultType" reference on the existing validate.eap entry corrected to "default_eap_type".
- CHANGELOG.md 1.2.0 §Added: createDefaultInstance, logging.*, isDaemonSet helper. §Changed: radiusd.conf body move (with escape hatches retained), log destination via configmap, podManagementPolicy default flip (with the IMMUTABLE-field caveat called out), snake_case batch, both cipher_list shape changes, EnvoyProxy access-log trim, firmansyah.id label strip, headless Service for DaemonSet, annotation rendering refactor, redis.metrics.extraArgs defensive shape. Same stale "defaultType" reference on line 358 fixed to "default_eap_type".
- README.md §Breaking Changes 1.2.0: extended inventory with remaining snake_case renames, modules.eap.tlsConfig.cipher_list shape, radiusd.conf body move, podManagementPolicy default flip; new §Upgrading entry "podManagementPolicy default is now Parallel (StatefulSet only)" with the kubectl delete sts --cascade=orphan recovery recipe and the "pin the old behavior in your overlay" alternative.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Releases freeradius chart 1.2.0. Full notes in charts/freeradius/CHANGELOG.md (long form) and Chart.yaml
artifacthub.io/changes(brief).Headline deltas vs. 1.1.0:
modules.oidc.*) — replaces the dedicated Keycloak module (now removed). Provider-agnostic (Keycloak / Authentik / Azure AD / Auth0 / Okta / …). Per-instance backends withgroupMappings/attributeMappings/require/introspect/refreshTokenCache. NAS binding viaclients.<x>.oidc: <name>.gateway.proxyProtocolknob renders an Envoy GatewayBackendTrafficPolicy(gateway.envoyproxy.io/v1alpha1) scoped to the RADSEC Service port and forcesproxy_protocol = yeson the RADSEClisten { }block, so FreeRADIUS setsPacket-Src-IP-Addressto the real client IP. v1 because FreeRADIUS 3.2.x only parses v1.sectionName— UDPRoute (auth/acct/coa) and TLSRoute (radsec)parentRefsnow name the listener explicitly instead of attaching to all compatible listeners on the parent Gateway.tls.autoGenerated/tls.certManager.create/modules.{sql.tls,eap.tlsConfig}.autoGenerateddeprecated.tls→radsec(BREAKING) — values, env-var, ConfigMap, and FreeRADIUS-internal block names all migrated. Top-leveltls.*(cert material) untouched.modules.redis.*+ standalonemodules.cache.*; NetworkPolicy egress narrowing;templates/gateway-api/EnvoyProxy.yaml+ Envoy Gateway data-plane infra knobs; ListenerSet (withXListenerSetv1alpha1 support);values.schema.json;.helmignore.Workload + topology:
kind:knob acceptsDeployment/StatefulSet/DaemonSet(case-insensitive). StatefulSet wires the headless Service viaserviceName:and switches persistence to per-replicavolumeClaimTemplates. Newfreeradius.isStatefulSet+freeradius.isDaemonSethelpers; headless Service now also renders for DaemonSet (same DNS-based per-pod discovery applies).templates/Service-perPod.yamlrenders one Service per StatefulSet ordinal; UDPRoute / TLSRoutebackendRefsfan out viafreeradius.gateway.backendRefs, preserving NAS source IP end-to-end with per-podexternalTrafficPolicy: Local.createDefaultInstance.{realm, homeServer, homeServerPool}— three opt-in toggles for chart-managed peer-mesh proxy primitives.realm DEFAULT+ per-podhome_server <fullname>_<ord>_<auth|acct>(per-pod Service DNS) +<fullname>_<auth|acct>_pool. All three silently skip on non-StatefulSet kinds. Shared secret rendered as literal$ENV{FREERADIUS_DEFAULT_INSTANCE_SECRET}; user wires viaextraEnvVarsSecret.podManagementPolicydefault flipped toParallelfor fresh StatefulSet installs. IMMUTABLE on live releases — k8s rejects in-place updates. See §Upgrading → podManagementPolicy in README.md for thekubectl delete sts --cascade=orphanrecovery recipe and the "pin the old behavior in your overlay" alternative.Config-as-values surface:
logging.*knobs — surface the FreeRADIUSlog { }directive set as values (destination/colourise/file/syslog_facility/stripped_names/auth/auth_badpass/auth_goodpass/msg_denied). Defaultdestination: stdoutmakeskubectl logswork out of the box.radiusd.confbody moved intotemplates/configmaps/configuration.yaml— single source of truth. Escape hatches preserved:.Values.configurations(inline string override) and.Values.configurationsConfigMap(BYO ConfigMap).files/radiusd.confdeleted (1214-line upstream-style file with wrong paths). The previously-hardcoded-l stdoutCLI override dropped — log destination is configmap-driven now.modules.sql.{readGroups, readProfiles}→read_groups/read_profilesmodules.json.encode.value.{singleValueAsArray, enumAsInteger, datesAsInteger, alwaysString}→ snake_casemodules.cache.maxEntries(base + per-instance) →max_entriesmodules.eap.{timerExpire, ignoreUnknownEapTypes, ciscoAccountingUsernameBug, maxSessions, defaultType}→ snake_case (includingdefault_eap_type)homeServers[]/homeServerPools[]/realms[]/sites.radsec.clients[]/modules.sql.{groupAttribute, readClients}renames covered earlier in the branch.modules.eap.tlsConfig.cipher_liststring →[]string(mirrorssites.radsec.tls.cipher_listshipped earlier). Joined with:viafreeradius.utils.joinOrDefault; default["DEFAULT"]; empty / nil falls through toDEFAULT..Values.architecturetop-level key removed (never referenced in any template; the onlyarchitecturehits intemplates/belong to the mariadb/postgresql subcharts).Removed (BREAKING — see Upgrading section in README):
keycloak.*block,KC_*/FREERADIUS_KEYCLOAK_*env vars, mods-config/keycloak ConfigMaps, allfreeradius.keycloak.*helpers,clients.<x>.keycloak).luamapper-script mode (rlm_lua not bundled infreeradius/freeradius-server:3.2.8).keycloak.mode: rest.Fixed:
apiVersion: falsewhen the API is absent.templates/configmaps/clients.yamlskips non-map scalar keys (includeFile,existingConfigMapName).image.debugactually gates-fvs-fxx.emptyDirat/var/run/radiusdforreadOnlyRootFilesystem: true.sites/inner-tunnel(was only emitted intosites/default). EAP-TTLS / PEAP tunnelled auth bound viaclients.<x>.oidc: <instance>now reachesoidc_<name>_authorize.-}}was eating the newline + leading indent, gluing theif (...) {directive onto the trailing comment line). Affected bothsites/defaultand the newly-wiredsites/inner-tunnel.Packet-Src-IP[v6]-Addresscomparison on attribute existence (&Attr && &Attr <= "...") — without it, IPv4-only requests bail withFailed casting lhs operandwhen the OR falls through to the IPv6 leg.<=IP-in-prefix operator instead of==for theclients.<x>.{ipv4addr,ipv6addr}match —==is exact-string equality, so any CIDR value silently missed.[*]multi-value iterator on attribute existence so a missing roles/groups claim doesn't bail withFailed retrieving values required to evaluate condition.Docs + tests:
modules.oidc.*(was Keycloak-shaped). ObsoleteFREERADIUS_KEYCLOAK_*env-var rows + bullets replaced withFREERADIUS_OIDC[_<NAME>]_CLIENT_SECRET.logging.*,extraStartupArgs,kind,podManagementPolicy,createDefaultInstance.{realm, homeServer, homeServerPool}.artifacthub.io/changesextended end-to-end.modules.cache.max_entries).Test plan
helm unittest charts/freeradius— 105/105 pass locally (10 suites: Application, GatewayAPI, HPA, Istio, Metrics, NetworkPolicy, OIDC, PersistentVolumeClaim, Service, schema).helm lint charts/freeradius— clean.artifacthub.io/changes, and README.md §Upgrading (to-1.2.0 subsection) describe the 1.1.0 → 1.2.0 deltas.ct lint+ helm-unittest on this PR.helm templatewith a representative values set (gateway.enabled=true,tls.enabled=true,modules.oidc.enabled=truewith at least one instance) renders cleanly.