fix(radius): run FreeRADIUS from a non-world-writable config dir #1556

Merged
k.renner merged 2 commits from fix/freeradius-confdir-mode into main 2026-11-04 08:08:25 +00:00
3 changed files with 107 additions and 13 deletions
@@ -16,7 +16,7 @@ data:
exec_prefix = ${prefix}
sbindir = ${exec_prefix}/sbin
libdir = ${exec_prefix}/lib
raddbdir = /etc/raddb
raddbdir = /run/raddb/raddb
confdir = ${raddbdir}
modconfdir = ${confdir}/mods-config
certdir = ${confdir}/certs
+39 -7
View File
@@ -36,6 +36,34 @@ spec:
fsGroup: 65534
seccompProfile:
type: RuntimeDefault
# FreeRADIUS refuses a config dir that is world-writable; kubelet's
# volume root is, so the config is copied to a dir the pod owns.
initContainers:
- name: raddb-copy
image: docker.io/freeradius/freeradius-server:3.2.10-alpine # {"$imagepolicy": "flux-system:freeradius"}
command:
- /bin/sh
- -ec
args:
- mkdir -p /run/raddb/raddb && cp -rL /etc/raddb-src/. /run/raddb/raddb/ && chmod 0750 /run/raddb/raddb
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
memory: 64Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: raddb
mountPath: /etc/raddb-src
readOnly: true
- name: raddb-run
mountPath: /run/raddb
containers:
- name: freeradius
image: docker.io/freeradius/freeradius-server:3.2.10-alpine # {"$imagepolicy": "flux-system:freeradius"}
@@ -44,7 +72,7 @@ spec:
- -l
- stdout
- -d
- /etc/raddb
- /run/raddb/raddb
env:
- name: DOT1X_NAS_SECRET
valueFrom:
@@ -73,14 +101,14 @@ spec:
command:
- /opt/bin/radclient
- -d
- /etc/raddb
- /run/raddb/raddb
- -q
- -r
- "1"
- -t
- "2"
- -f
- /etc/raddb/probe/status
- /run/raddb/raddb/probe/status
- 127.0.0.1:18121
- status
- status-probe
@@ -91,14 +119,14 @@ spec:
command:
- /opt/bin/radclient
- -d
- /etc/raddb
- /run/raddb/raddb
- -q
- -r
- "1"
- -t
- "2"
- -f
- /etc/raddb/probe/status
- /run/raddb/raddb/probe/status
- 127.0.0.1:18121
- status
- status-probe
@@ -118,8 +146,8 @@ spec:
drop:
- ALL
volumeMounts:
- name: raddb
mountPath: /etc/raddb
- name: raddb-run
mountPath: /run/raddb
readOnly: true
- name: tmp
mountPath: /tmp
@@ -158,6 +186,10 @@ spec:
path: certs/radius-tls/tls.crt
- key: tls.key
path: certs/radius-tls/tls.key
- name: raddb-run
emptyDir:
medium: Memory
sizeLimit: 4Mi
# pidfile
- name: tmp
emptyDir:
+67 -5
View File
@@ -33,6 +33,9 @@ cleanup() {
mapfile -t ids < <(docker ps -aq --filter name='^radtest-')
if ((${#ids[@]} > 0)); then docker rm -f "${ids[@]}" >/dev/null; fi
if [[ -n "$(docker network ls -q --filter name="^${net}\$")" ]]; then docker network rm "$net" >/dev/null; fi
local vols=()
mapfile -t vols < <(docker volume ls -q --filter name='^radtest-')
if ((${#vols[@]} > 0)); then docker volume rm "${vols[@]}" >/dev/null; fi
rm -rf "$work"
}
trap cleanup EXIT
@@ -67,6 +70,59 @@ render() {
openssl x509 -req -in "$work/pki/server.csr" -CA "$work/eapol/ca.pem" -CAkey "$work/pki/ca.key" \
-CAcreateserial -days 1 -extfile "$work/pki/ext" -out "$raddb/$crt" >>"$work/pki/openssl.log" 2>&1
chmod -R a+rX "$work"
# kubelet creates the projected volume root world-writable
chmod 1777 "$raddb"
}
# Volume mounts of a pod container as docker -v options, as the pod declares
# them: the projected volume is the rendered config, other emptyDirs (except
# the /tmp tmpfs) are named docker volumes.
mount_opts() { # container name; sets mounts[]
local vol name path ro ro_suffix
vol="$(deployment | yq -r '.spec.template.spec.volumes[] | select(.projected) | .name')"
mounts=()
while read -r name path ro; do
ro_suffix=""
if [[ "$ro" == true ]]; then ro_suffix=":ro"; fi
if [[ "$name" == "$vol" ]]; then
mounts+=(-v "$work/raddb:$path${ro_suffix}")
elif [[ "$name" != tmp ]]; then
mounts+=(-v "radtest-$name:$path${ro_suffix}")
fi
done < <(deployment | yq -r --arg n "$1" '(.spec.template.spec.containers + (.spec.template.spec.initContainers // []))[]
| select(.name == $n) | .volumeMounts[] | "\(.name) \(.mountPath) \(.readOnly // false)"')
}
# emptyDir volumes other than /tmp, writable by the pod user like kubelet's
create_volumes() {
local name
while read -r name; do
docker volume create "radtest-$name" >/dev/null
docker run --rm --user 0:0 -v "radtest-$name:/v" "$mock_image" chmod 0777 /v >/dev/null
done < <(deployment | yq -r '.spec.template.spec.volumes[] | select(.emptyDir) | select(.name != "tmp") | .name')
}
# Pod restrictions: non-root, read-only root, no capabilities.
pod_opts=(--user 65534:65534 --read-only --tmpfs /tmp:rw,size=16m --cap-drop ALL --security-opt no-new-privileges)
# Run the Deployment's initContainers exactly as defined.
run_init() {
local names=() n image cmd=() args=()
mapfile -t names < <(deployment | yq -r '.spec.template.spec.initContainers // [] | .[].name')
for n in "${names[@]}"; do
image="$(deployment | yq -r --arg n "$n" '.spec.template.spec.initContainers[] | select(.name == $n) | .image')"
mapfile -t cmd < <(deployment | yq -r --arg n "$n" '.spec.template.spec.initContainers[] | select(.name == $n) | .command[]')
mapfile -t args < <(deployment | yq -r --arg n "$n" '.spec.template.spec.initContainers[] | select(.name == $n) | .args // [] | .[]')
mount_opts "$n"
if docker run --rm --name "radtest-init-$n" --network "$net" "${pod_opts[@]}" "${mounts[@]}" \
--entrypoint "${cmd[0]}" "$image" "${cmd[@]:1}" "${args[@]}" >"$work/init-$n.log" 2>&1; then
pass "initContainer $n"
else
cat "$work/init-$n.log"
fail "initContainer $n"
exit 1
fi
done
}
# --- dummy secrets for every secretKeyRef env var of the Deployment
@@ -106,13 +162,19 @@ start() {
-v "$here/mock_outpost.py:/mock_outpost.py:ro" \
"$mock_image" python3 -u /mock_outpost.py >/dev/null
# Same restrictions as the pod: non-root, read-only root, no capabilities.
local run_opts=(--network "$net" --user 65534:65534 --read-only --tmpfs /tmp:rw,size=16m
--cap-drop ALL --security-opt no-new-privileges "${secret_env[@]}"
-v "$work/raddb:/etc/raddb:ro")
create_volumes
run_init
# Same restrictions as the pod.
mount_opts freeradius
local run_opts=(--network "$net" "${pod_opts[@]}" "${secret_env[@]}" "${mounts[@]}")
local confdir="" i
for i in "${!args[@]}"; do
if [[ "${args[$i]}" == -d ]]; then confdir="${args[$((i + 1))]}"; fi
done
echo "--- radiusd -XC (config check)"
if docker run --rm "${run_opts[@]}" "$image" radiusd -XC -d /etc/raddb >"$work/configcheck.log" 2>&1 \
if docker run --rm "${run_opts[@]}" "$image" radiusd -XC -d "$confdir" >"$work/configcheck.log" 2>&1 \
&& grep -q "Configuration appears to be OK" "$work/configcheck.log"; then
pass "radiusd -XC: Configuration appears to be OK"
else