RADIUS Servers
Kaniop deploys and operates Kanidm’s RADIUS component (FreeRADIUS plus the Kanidm authentication
module) using the KanidmRadiusServer Custom Resource. One resource gives you one independently
managed RADIUS endpoint: a dedicated Kanidm service account, a read-only API token, a Deployment and
a UDP Service. You can run several resources against one Kanidm cluster, for example one per site
with different NAS devices, VLAN policy and Service exposure.
Kaniop manages the server. It does not provision NAS devices and does not create or reset users’ RADIUS passwords (those stay in Kanidm, see Users and RADIUS passwords).
Prerequisites
- A
Kanidmcluster managed by Kaniop, running a release whosekanidm/radiusimage Kaniop can pair with it (see Version pairing). - The Kaniop release that includes the
KanidmRadiusServerCRD. When upgrading, apply the chart’s CRDs as described in Installation. -
The
KanidmRadiusServernamespace must be authorized by theKanidmresource throughspec.radiusServerNamespaceSelector. Without it only the Kanidm’s own namespace is permitted:apiVersion: kaniop.rs/v1beta1 kind: Kanidm metadata: name: my-idm namespace: identity spec: domain: idm.example.com # Absent: only the namespace of this Kanidm. {}: every namespace. radiusServerNamespaceSelector: matchLabels: kubernetes.io/metadata.name: networkA
KanidmRadiusServerin a namespace that is not selected gets aResourceNotWatchedWarning event,IdentityReady=Falsewith reasonNamespaceNotWatched, and no finalizer. The selector is independent ofserviceAccountNamespaceSelector. - A Secret with one shared secret per NAS, a Secret with the EAP server certificate and key, and
(for private PKI) a CA bundle for the Kanidm endpoint. All must be in the same namespace as
the
KanidmRadiusServer. - Operator RBAC. The Helm chart’s ClusterRole already grants what the controller needs: read access
to Pods and ReplicaSets (to find which pods still use a token or config Secret), and management
of Deployments, Services, Secrets and Events. If you maintain the operator’s RBAC yourself, add
get,listandwatchonapps/replicasetswhen upgrading from a release without RADIUS support. - RADIUS pods must be able to reach the Kanidm URL over HTTPS (see How the Kanidm URL is derived).
Example
The generated reference, with every option documented, is
examples/radius-server.yaml.
A complete working set, in the network namespace:
apiVersion: v1
kind: Secret
metadata:
name: nas-radius-secrets
namespace: network
type: Opaque
stringData:
# 16-255 characters of [A-Za-z0-9_-] only, for example:
# openssl rand -base64 24 | tr '+/' '-_' | tr -d '='
mikrotik: REPLACE-WITH-A-GENERATED-SECRET
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: radius-eap-tls
namespace: network
spec:
secretName: radius-eap-tls
dnsNames:
- radius.example.com
usages:
- server auth
- digital signature
- key encipherment
issuerRef:
name: internal-ca
kind: ClusterIssuer
---
apiVersion: kaniop.rs/v1beta1
kind: KanidmRadiusServer
metadata:
name: campus-radius
namespace: network
spec:
kanidmRef:
name: my-idm
namespace: identity
replicas: 2
authorization:
requiredGroups:
- radius_access_allowed@idm.example.com
defaultVlan: 10
groupVlans:
- group: staff@idm.example.com
vlan: 20
clients:
- name: mikrotik
ipaddr: 192.0.2.10/32
sharedSecretRef:
name: nas-radius-secrets
key: mikrotik
radiusTls:
secretRef:
name: radius-eap-tls
caKey: ca.crt
kanidmTls:
caBundleRef:
kind: ConfigMap
name: kanidm-ca
key: ca.crt
service:
type: LoadBalancer
port: 1812
externalTrafficPolicy: Local
tokenRotation:
enabled: true
periodDays: 90
Check progress with:
kubectl get kanidmradiusservers -n network # short name: krs
kubectl describe krs campus-radius -n network
The Ready column is true when IdentityReady, ConfigurationReady, Available and Exposed are
all True; the Endpoint column shows the address NAS devices should use.
Naming and limits:
- The resource name must be at most 52 characters (the generated config Secret is
<name>-<10 hex characters>). - The Deployment and the Service are both named after the resource.
spec.kanidmRefis immutable. To move a server to another Kanidm, delete and create it.replicasdefaults to 2 (range 1 to 100). Rollouts usemaxSurge: 1andmaxUnavailable: 0.
Users and RADIUS passwords
Kaniop does not manage user credentials. Every person who authenticates over RADIUS needs a RADIUS
secret generated in Kanidm, separate from their normal password, and membership in one of the
requiredGroups:
kanidm person radius generate-secret alice
NAS clients and shared secrets
Each entry in spec.clients lets one NAS (or one network) talk to the server.
| Field | Meaning |
|---|---|
name |
FreeRADIUS client name, A-Za-z0-9_-, 1 to 63 characters, unique. |
ipaddr |
IPv4 or IPv6 address, or a CIDR with a non-zero prefix. 192.0.2.10 and 192.0.2.10/32 are the same client. Host bits of a CIDR are masked. |
sharedSecretRef |
name and key of a Secret in the same namespace. |
Rules enforced when the inputs are read:
- The shared secret is never in the resource. The referenced value must be 16 to 255 characters
from
A-Za-z0-9_-. The upstream image writes it unquoted into the FreeRADIUSclients.conf, so whitespace, quotes,#, braces and similar characters are rejected rather than escaped. Configure the same value on the NAS device. - Client names must be unique, and so must the resulting networks (duplicates are rejected, not merged).
- A request from an address that matches no client is dropped by FreeRADIUS. When the NAS reports
authentication timeouts, first check that the source address the pod sees matches
ipaddr(see Service exposure).
Rotating a NAS secret means updating the Secret. Kaniop watches the referenced Secrets, renders a new config Secret and rolls the Deployment. No API token is minted or revoked by that change. Update the NAS device at the same time, since in-flight devices use the old secret until they are changed.
Message-Authenticator (BlastRADIUS)
Configure every NAS to send the Message-Authenticator attribute in all Access-Request packets.
This is the mitigation for the BlastRADIUS attack on RADIUS/UDP (CVE-2024-3596), and the server
enforces it:
- The upstream
kanidm/radiusimage writesrequire_message_authenticator = yesinto every generated FreeRADIUSclientblock. This was checked against thekanidm/radius:1.11.2image: running its bootstrap on a one-clientradius.tomlproduced that line in the client block. FreeRADIUS discards an Access-Request from such a client that has noMessage-Authenticator. Nothing is sent back, so the NAS sees a timeout, not a reject. - Kaniop has no setting to relax this, and the image does not write
limit_proxy_stateper client. The per-clientyesoverrides the FreeRADIUS global default (auto). Kaniop does not change the image’s behaviour, so it follows whatever the image version you run does. - EAP exchanges carry
Message-Authenticatorby specification, so EAP-TLS and PEAP devices normally comply. Check older firmware and PAP/CHAP-only devices, which may not send it. Update the firmware or the device’s RADIUS settings rather than looking for a way to turn the check off.
This protects requests reaching the server. Keep NAS-to-server traffic on a trusted network segment and use a long, random shared secret, because UDP RADIUS has no transport protection of its own.
An illustrative MikroTik RouterOS client definition (adapt to your device and EAP setup):
/radius add service=wireless address=<service address> secret=<shared secret> authentication-port=1812
Authorization and VLANs
spec:
authorization:
requiredGroups:
- radius_access_allowed@idm.example.com
defaultVlan: 10
groupVlans:
- group: staff@idm.example.com
vlan: 20
requiredGroups(1 to 100 entries, required): Kanidm group SPNs (name@domain). A user must be a member of at least one of them to be accepted. The list cannot be empty, which prevents a server that accepts every Kanidm user by accident.defaultVlan(default 1, range 1 to 4094): VLAN returned to permitted users that match no entry ingroupVlans.groupVlans(up to 100 entries): maps a group to a VLAN (1 to 4094). It is independent ofrequiredGroups: a group mapped here does not by itself grant access. Avoid overlapping mappings for the same user, since the choice between them is made by the upstream module.
Changing authorization rolls the Deployment (new config Secret, same token).
EAP TLS with cert-manager
spec.radiusTls points at a Secret with the certificate chain and private key FreeRADIUS presents
to supplicants. The upstream image requires them, and Kaniop does not issue certificates.
| Field | Default | Meaning |
|---|---|---|
secretRef.name |
required | Same-namespace Secret. |
certKey |
tls.crt |
Key with the PEM certificate chain. |
privateKeyKey |
tls.key |
Key with the matching PEM private key. |
caKey |
none | Optional key in the same Secret with the CA that supplicants are expected to trust. |
A cert-manager Certificate writes exactly these keys, plus ca.crt when the issuer provides one,
so the example above only adds caKey: ca.crt. Use a certificate that your supplicants will
validate (typically its DNS name is the RADIUS server name configured in the supplicant profile).
Before any change is applied, Kaniop checks that the chain parses, the key matches the certificate,
and the certificate is currently valid. An invalid or expired certificate reports
ConfigurationReady=False with reason InvalidTlsMaterial; the running pods and token are left
untouched.
When cert-manager renews the certificate, Kaniop notices the Secret change and rolls the Deployment (a digest of the public certificate and CA bytes is part of the pod template). No token is minted or revoked. The private key is read to validate it and is passed to the pods through the Secret mount; it is never copied into status or events.
Kanidm CA trust
If Kanidm’s HTTPS endpoint uses a private CA, point the RADIUS pods at the CA bundle:
spec:
kanidmTls:
caBundleRef:
kind: ConfigMap # or Secret; default ConfigMap
name: kanidm-ca
key: ca.crt
The ConfigMap or Secret must be in the namespace of the KanidmRadiusServer, not the Kanidm
namespace. Certificate and hostname verification are always on; there is no option to disable
them. Without caBundleRef the pods use the image’s system trust store. A missing object reports
KanidmCaNotFound, an unreadable or empty bundle KanidmCaInvalid.
How the Kanidm URL is derived
There is no URL field on KanidmRadiusServer. Pods connect to the referenced Kanidm’s
spec.origin when set, otherwise https://<spec.domain>. Consequences:
- The name in that URL must resolve from inside the RADIUS pods and be covered by Kanidm’s
certificate. A placeholder domain such as
my-idm.localhost(used in the generated example) will not work from another pod. - For split-horizon or non-default-port setups, express them on the
Kanidmresource: setspec.origin(for examplehttps://idm.example.com:8443) or make DNS inside the cluster resolve the Kanidm domain to the Kanidm Service. Note thatoriginis also Kanidm’s WebAuthn origin, so choose it deliberately.
Service exposure and source addresses
spec:
service:
type: ClusterIP # ClusterIP (default), NodePort or LoadBalancer
port: 1812 # Service port, default 1812 (the container always listens on 1812)
externalTrafficPolicy: Local # NodePort and LoadBalancer only; rejected on ClusterIP
annotations: {} # e.g. cloud or MetalLB load balancer annotations
The Service is UDP. Make sure your load balancer implementation supports UDP, and that any NetworkPolicy or firewall allows UDP to the pods on 1812 from your NAS devices and egress from the pods to Kanidm over HTTPS.
| Type | Use it when | status.endpoint |
|---|---|---|
ClusterIP |
The NAS is inside the cluster network. The CRD rejects externalTrafficPolicy on a ClusterIP Service (as Kubernetes does), and the controller reports InvalidSpec if one gets through. |
<clusterIP>:<port> |
NodePort |
You front the cluster with your own load balancer or address any node. | *:<nodePort> (any node) |
LoadBalancer |
Your platform assigns an external address. | <address>:<port> once assigned |
FreeRADIUS authenticates a NAS by the source IP of the packet and uses the shared secret of the
client that matches it. This makes externalTrafficPolicy important for NAS devices outside the
cluster:
Cluster(the Kubernetes default when the field is omitted): the node that receives the packet may forward it to a pod on another node and rewrites the source address (SNAT). The pod then sees a node address, not the NAS. Per-NASipaddrentries no longer match and requests are dropped. It only works if yourclientslist the node or SNAT addresses (which collapses every NAS into one client and one shared secret) or one CIDR that covers them.Local: the source address is preserved and traffic is delivered only to pods on the receiving node. Nodes without a ready pod must not receive traffic: load balancer health checks do this on clouds that support it, but with MetalLB orNodePortyou must ensure every node that can receive traffic runs a pod, usingreplicas,topologySpreadConstraintsoraffinity.
Prefer Local and exact ipaddr entries when you have per-NAS secrets. Check the actual source
address in the pod logs if a NAS is silently ignored.
Pod lifecycle and rollouts
RADIUS runs over UDP, so a NAS that sends to a pod that is already gone gets no answer and retries later. The pods are built so that rotations and rollouts do not cause that:
- A
preStophook sleeps for 10 seconds beforeradiusdreceivesSIGTERM. A terminating pod keeps answering UDP while the endpoint removal propagates to EndpointSlices and kube-proxy (or your CNI), so NAS retries are served instead of dropped. terminationGracePeriodSecondsis 30, longer than the hook, soradiusdis stopped cleanly and not killed during the sleep.- Rollouts use
maxSurge: 1andmaxUnavailable: 0: an old pod is removed only after a new pod is ready. Token rotation, NAS secret changes and configuration changes all roll this way.
Neither value is configurable. A pod takes about 10 seconds longer to terminate, so a rollout of N
replicas takes correspondingly longer, and so does deletion (see
Deleting a server). Keep replicas at 2 or more if you need uninterrupted
service during a rollout or a node drain.
Security model
- Dedicated identity per resource. Each
KanidmRadiusServerowns one Kanidm service account namedkaniop_radius_<uid>where<uid>is the resource’s Kubernetes UID without hyphens. The display name is the ownership markerKaniop RADIUS: <namespace>/<name> [<uid>]. Re-creating a resource with the same name produces a new UID and therefore a new account; the old account is never adopted. - Additive group membership. The account is added to the built-in
idm_radius_serversgroup with a single member edge. Kaniop never calls set or purge on that group, so other RADIUS servers and unrelated members are untouched. Do not manageidm_radius_serversthrough aKanidmGroupwithmembers, since that is authoritative and would remove the Kaniop accounts. - Read-only API token. The pods authenticate to Kanidm with a read-only token. It is not mounted
as a Kubernetes service account token (
automountServiceAccountTokenis off). - Where the secrets live. The token and the NAS shared secrets are rendered into one Kubernetes
Secret per configuration. They are never in the resource,
status, events or operator logs; status carries token ids only. Restrictgeton Secrets in that namespace accordingly. - Encryption at rest and RBAC. The per-resource config Secret holds the Kanidm API token and
the NAS shared secrets in clear text. Anyone who can read it (through the API, an etcd backup or a
node) can authenticate to Kanidm as the RADIUS account and impersonate your NAS devices. Enable
Kubernetes encryption at rest for Secrets
(or a KMS provider), protect etcd backups, and grant
get,listandwatchon Secrets in the namespace only to the operator and to people who need them (listandwatchreturn Secret contents too). Kaniop cannot enforce any of this. - Immutable, content-addressed config Secrets. The config Secret is named
<name>-<first 10 hex characters of the digest of its content>, is created once and is never updated. A configuration change creates a new Secret and rolls the Deployment; the old Secret is garbage-collected only when no pod, ReplicaSet or template references it. - Foreign objects are not taken over. If an object with a name Kaniop needs (Deployment, Service,
config Secret, Kanidm account) exists and is not controlled by this resource, Kaniop reports
ForeignObject,SecretConflictorOwnershipConflictand leaves it alone. - Pod hardening. Pods run as UID 497 and GID 496 (the user of the verified image), non-root,
with all capabilities dropped, privilege escalation disabled and the
RuntimeDefaultseccomp profile. The projected configuration volume is mounted read-only with mode 0440.
Why the root filesystem stays writable
readOnlyRootFilesystem is intentionally not enabled. The upstream image bootstrap rewrites
/etc/raddb/clients.conf and writes /etc/raddb/certs/{ca,server}.pem when it starts. A tmpfs
over /etc/raddb/certs is not enough because clients.conf is a file in /etc/raddb. Making
/etc/raddb writable from an emptyDir needs an init container to copy it, and radiusd then refuses
to start because the directory is globally writable (0777 mount point) unless a root init container
fixes its mode. That trade is worse than a writable container layer. Revisit this only if upstream
stops writing under /etc/raddb.
Token lifecycle
The API token is derived from what Kanidm and Kubernetes actually contain on every reconcile, not
from remembered state, so losing status or a Secret never wedges the resource.
- Mint. A new token is created when the Deployment template has none (
Initial), when the token in use cannot be read or no longer exists in Kanidm (BearerLost), when it expires within one day (Expired), when it is older thanperiodDays(Due), or when a manual rotation is requested (Requested). - Verify before rollout. The new token must pass a
whoamicall against Kanidm (retried briefly for replica lag). A token that is rejected is never written to a Secret or pod; the conditionRotatingshowsCandidateRejectedand the existing token keeps serving. This proves the token is valid, not that a RADIUS login works: the readiness probe isradiusd -C, a configuration check. The rolling update withmaxUnavailable: 0keeps the old pods serving until new pods are ready. - Roll out. A new config Secret holds the new token and the Deployment template points to it.
- Revoke only when unused. The old token is revoked only after no pod (including terminating
ones), ReplicaSet or the template references it, and it is more than 10 minutes old (grace
for a crashed or in-flight reconcile). If Kaniop cannot work out which token some consumer
holds, it revokes nothing until that is resolved (
ConsumerUnresolved). Tokens kept this way are listed instatus.retiringTokenIds. - Bound. The account never holds more than 4 tokens. If a new token is needed while 4 exist,
Kaniop reports
TooManyTokensand keeps revoking unused tokens until it can proceed.
An unreachable Kanidm never causes a revocation.
Rotation settings
spec:
tokenRotation:
enabled: true # default false
periodDays: 90 # 1 to 365, default 90
- With
enabled: false(the default), the token never expires and is replaced only for the other reasons above. - With
enabled: true, new tokens expire after 2 xperiodDays(180 days with the default of 90) and are replaced afterperiodDays, which leaves a full period for a failing rotation to be noticed and fixed before the token lapses. tokenRotationuses the sameenabled/periodDaystype as the other Kaniop resources. The CRD does not boundperiodDays; Kaniop rejects a value outside 1 to 365 withInvalidSpec.
Manual rotation
Set the annotation kaniop.rs/radius-rotate-token to any new non-empty value:
kubectl annotate krs campus-radius -n network \
kaniop.rs/radius-rotate-token="$(date +%s)" --overwrite
A rotation happens when the value differs from the one recorded with the current token. Setting the same value again, or removing the annotation afterwards, does not rotate again. Use a new value for each rotation. If a token may have leaked, rotate and then follow the break-glass runbook to destroy the old tokens immediately instead of waiting for the unused-token revocation.
Status and conditions
All five conditions are always present.
| Condition | True when |
|---|---|
IdentityReady |
The Kanidm account exists, is owned by this resource and is in idm_radius_servers. |
ConfigurationReady |
Every referenced Secret, CA and certificate is valid and the workload could be rendered. |
Available |
The rollout is complete and all desired replicas are available. |
Exposed |
The Service has an address (cluster IP, node port or load balancer address). |
Rotating |
A token change is in progress, or an old token is being kept. False (TokenCurrent) is the steady state. It does not affect status.ready. |
Other status fields: endpoint, tokenId, tokenIssuedAt, configSecretName, deploymentName,
serviceName, replicas, readyReplicas, retiringTokenIds, observedGeneration. A condition
that was not reached in the last pass is Unknown with reason NotEvaluated. Transient API errors
(Kubernetes or Kanidm) appear as Unknown/TransientError and are retried with backoff.
Reconciliation pauses (without status changes) while a Kanidm restore is in progress.
Troubleshooting by reason
kubectl get krs <name> -n <ns> -o jsonpath='{range .status.conditions[*]}{.type}{"\t"}{.status}{"\t"}{.reason}{"\t"}{.message}{"\n"}{end}'
| Condition | Reason | Meaning and fix |
|---|---|---|
IdentityReady |
KanidmNotFound |
spec.kanidmRef names no Kanidm. Fix the reference (it is immutable: recreate the resource). |
IdentityReady |
NamespaceNotWatched |
The Kanidm’s radiusServerNamespaceSelector does not select this namespace. Update the Kanidm. Running pods keep their token but it is not rotated while the namespace is not selected. |
IdentityReady |
InvalidIdentity |
The resource lacks a namespace, name or UID. Recreate it. |
IdentityReady |
OwnershipConflict |
A Kanidm account named kaniop_radius_<uid> exists without this resource’s marker. It is not touched. Investigate who created it. |
IdentityReady |
AccountReady |
Healthy. |
ConfigurationReady |
InvalidSpec |
A spec value is out of range, or externalTrafficPolicy is set on a ClusterIP Service (the CRD already rejects this at admission). |
ConfigurationReady |
NasSecretNotFound, NasSecretKeyInvalid |
A sharedSecretRef Secret or key is missing or not UTF-8. |
ConfigurationReady |
TlsSecretNotFound, TlsSecretKeyMissing |
The EAP certificate Secret or one of its keys is missing. |
ConfigurationReady |
InvalidTlsMaterial |
Certificate or key unparsable, mismatched, not yet valid or expired. Check the cert-manager Certificate. |
ConfigurationReady |
KanidmCaNotFound, KanidmCaInvalid |
The CA ConfigMap or Secret is missing, or has no valid certificates. |
ConfigurationReady |
InvalidConfiguration |
A shared secret, client name, address, group or VLAN is invalid (see NAS clients). |
ConfigurationReady |
ImageUnresolved |
The RADIUS image cannot be derived. Set spec.image. A server that already runs keeps its current image, so token rotation goes on meanwhile. |
ConfigurationReady |
InvalidWorkload |
The Deployment or Service could not be built from the spec. |
ConfigurationReady |
SecretConflict |
A config Secret with the same name but different content exists. Delete the stray Secret. |
ConfigurationReady |
ConfigurationValid |
Healthy. |
Available |
DeploymentMissing, RolloutInProgress, ReplicasUnavailable |
Rolling out or pods not ready. Check kubectl describe pods -l kaniop.rs/radius-uid=<uid>. |
Available |
ProgressDeadlineExceeded |
The rollout is stuck: image pull, missing Secret mounts, or scheduling. |
Available |
ForeignObject |
A Deployment with this name exists and is not controlled by this resource. |
Available |
RolloutComplete |
Healthy. |
Exposed |
ServiceMissing, ClusterIPPending |
The Service has not been created or assigned an IP yet. |
Exposed |
LoadBalancerPending |
The platform has not assigned an address. Check the cloud or MetalLB controller. |
Exposed |
NodePortPending |
No node port assigned yet. |
Exposed |
ForeignObject |
A Service with this name exists and is not controlled by this resource. |
Exposed |
ClusterIPAssigned, NodePortAssigned, LoadBalancerReady |
Healthy. |
Rotating |
CandidateRejected |
Kanidm rejected the new token (whoami). The old token still serves; retried every 30 s. Check Kanidm health and replica lag. |
Rotating |
MintedTokenNotFound |
The new token could not be identified on the account. It is cleaned up as an orphan after 10 minutes. |
Rotating, ConfigurationReady |
ConcurrentMutation |
An object changed while it was being modified (a retired config Secret, or the new one). Retried within seconds. |
Rotating |
TooManyTokens |
The account holds 4 tokens and none is unused yet. Retried every minute; clears itself as old tokens are revoked. |
Rotating |
BearerUnreadable |
The planner chose to keep the current token but the config Secret the Deployment mounts has no readable token at that moment (a race with a Secret change). Retried on the next pass; if it persists, inspect the Secret named in status.configSecretName. |
Rotating |
ConsumerUnresolved |
Kaniop cannot tell which token a pod or ReplicaSet uses, so it holds all revocation. Look for a pod mounting a deleted config Secret or carrying a bad kaniop.rs/radius-token-id annotation. |
Rotating |
Initial, BearerLost, Expired, Due, Requested, RetiringTokens |
A rotation just happened (reason: why) or old tokens are still in use. Normal. |
Rotating |
TokenCurrent |
Steady state. |
| any | MissingMetadata |
An object lacks a name, namespace, UID or resourceVersion Kaniop needs. Reported on the condition of the step that failed. |
| any | TransientError |
A Kubernetes or Kanidm call failed; see the message and operator logs. |
| any | NotEvaluated |
An earlier step failed first. Fix the first non-healthy condition. |
Other useful checks:
kubectl get events -n <ns> --field-selector involvedObject.name=<name>
kubectl logs deployment/<name> -n <ns>
kubectl logs deployment/kaniop -n kaniop | grep -i radius
A failed input never disturbs a running endpoint: pods and token stay as they were until the input is valid again.
While a block stops reconciliation before the Deployment is updated (an invalid input,
NamespaceNotWatched, KanidmNotFound, TooManyTokens and the like) the token cannot be rotated. When the token the pods use expires
within two days (twice the one-day safety margin), each blocked pass emits a TokenNearExpiry
Warning event naming the token, its expiry and the block; clear the block before the token lapses.
If the last pass did not read the token from Kanidm, the expiry is estimated from
status.tokenIssuedAt and the current tokenRotation (issued + 2 x periodDays).
Version pairing with the RADIUS image
The RADIUS image is released separately from kanidm/server and a server digest is not a RADIUS
digest. Kaniop therefore infers the image only in one case: when the referenced Kanidm runs
kanidm/server:1.11.2 from Docker Hub (also spelled docker.io/ or index.docker.io/), the RADIUS
image is kanidm/radius:1.11.2. That is the only release whose runtime user (UID 497, GID 496) and
bootstrap have been verified. For any other tag, a digest, or another registry, set the image
explicitly or the resource reports ImageUnresolved (a server that already runs keeps its current
image until the image is fixed):
spec:
image: registry.example.com/kanidm/radius:1.11.2
An explicit image is used as written. Keep the RADIUS image on the same release as Kanidm, and verify the UID and GID of any other release before using it.
Migrating from a manual RADIUS deployment
Kaniop never adopts existing accounts, tokens or Deployments. Migrate side by side:
- Create the
KanidmRadiusServerwith a different Service address from the old server (another load balancer address or port). It creates its own account and token, so there is nothing to clean up in the old deployment. - Wait for
Ready, then point one test NAS at the new endpoint and verify authentication, VLAN assignment and denial of a user outsiderequiredGroups. - Move the remaining NAS devices (or move the address) one by one.
- Retire the old deployment, and manually destroy its API token and remove its account from
idm_radius_serversin Kanidm. Kaniop does not know about them.
Both deployments can use idm_radius_servers at the same time, because Kaniop only adds and removes
its own account.
Deleting a server
On deletion the finalizer: deletes the Deployment and waits for its pods to terminate, waits until no pod or ReplicaSet uses the config Secrets or tokens, removes the account’s membership edge, deletes the account (which destroys all its tokens), and finally lets Kubernetes garbage-collect the Service and config Secrets. External Secrets (NAS, TLS, CA) are never deleted.
While deletion waits for pods or ReplicaSets, the resource gets an AwaitingConsumers Warning event
naming them (for example pod/campus-7d9f-abcde), when the set changes and at most every 10 minutes
otherwise. A pod that is not part of the Deployment but mounts a config Secret (a debug pod, a
copy) blocks deletion until it is removed.
Two situations skip the Kanidm part and still remove the finalizer, with a Warning event so the leftover can be cleaned up by hand:
KanidmNotFound: the referencedKanidmno longer exists, so its RADIUS account could not be cleaned up.AccountNotOwned: the account exists without this resource’s marker and is left untouched.
A Kanidm that exists but is unreachable is retried with backoff, not skipped.
Break-glass runbook
Use this only when the normal finalizer cannot finish and you need the resource gone, or when you must cut off a RADIUS token immediately. Normal deletion needs none of it.
Warning. Members of
idm_radius_serverscan read users’ RADIUS credentials in Kanidm. Clearing the finalizer before the account is deleted leaves a live account with that access, and any tokens it still has, in Kanidm with nothing left to clean it up. Clear the finalizer last.
You need a Kanidm account allowed to manage service accounts and the group, such as idm_admin. Its
password is in the <kanidm>-admin-passwords Secret created by Kaniop (see
Quickstart). The commands below use the kanidm CLI; check
kanidm <command> --help for your version.
-
Identify the account. The name comes from the resource’s UID:
UID_=$(kubectl get krs campus-radius -n network -o jsonpath='{.metadata.uid}') ACCOUNT="kaniop_radius_${UID_//-/}" kanidm login -H https://idm.example.com -D idm_admin kanidm service-account get "$ACCOUNT"If the resource is already gone, list candidates with
kanidm service-account list | grep kaniop_radius_. -
Verify the marker. The
displaynamemust be exactlyKaniop RADIUS: <namespace>/<name> [<uid>]with the UID with hyphens. If it is not, or the account does not exist, the account is not ours: skip steps 4 to 6 and go to step 7 (this is theAccountNotOwnedcase, and Kaniop would not have touched it either). -
Stop the pods. Otherwise they keep serving with tokens you are about to destroy, and fail once Kanidm rejects them:
kubectl delete deployment campus-radius -n network --waitIf the operator is running and the resource is not being deleted, it recreates the Deployment. Delete the resource first (it will stay in
Terminatingon its finalizer) or scale the operator down. -
List and destroy the tokens. Destroying the token ids is enough to cut access even if you stop here (for example when a token leaked):
kanidm service-account api-token status "$ACCOUNT" kanidm service-account api-token destroy "$ACCOUNT" <token-id> -
Remove the membership edge. Remove only this account. Never use
set-membersorpurge-memberson this group:kanidm group remove-members idm_radius_servers "$ACCOUNT" -
Delete the account. This also removes any token that is left:
kanidm service-account delete "$ACCOUNT" -
Clear the finalizer. Only after the account is deleted or confirmed not ours:
kubectl patch krs campus-radius -n network --type=merge \ -p '{"metadata":{"finalizers":null}}'This removes every finalizer on the resource. If other controllers add finalizers you want to keep, patch by index with a JSON patch (
removeon/metadata/finalizers/<index>) after checkingkubectl get krs campus-radius -n network -o jsonpath='{.metadata.finalizers}'. The finalizer Kaniop uses iskanidmradiusservers.kaniop.rs/finalizer.
When it is safe to clear the finalizer: the account is deleted, or the marker check showed it is not this resource’s; the Deployment is gone, so no pod depends on the tokens; and you accept that the Kubernetes children are garbage-collected afterwards (Service and config Secrets). If the Kanidm is merely unreachable, prefer to wait: the finalizer retries on its own and cleans up properly once it returns.