Compare commits

..
Author SHA1 Message Date
claude-botandClaude Opus 5.5 ed3be3156b feat(network): VLAN 11 link radius0 on the nodes, Cilium VLAN bypass
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
VLAN 11 "(11) RADIUS" carries the FreeRADIUS proxy traffic to the UDM
RADIUS, which accepts clients from its own networks only.

- Talos k8s-{1,2,3}: VLANConfig radius0 (VLAN 11 on uplink), link only,
  no addresses, IPv6 disabled (VLAN 11 sends RAs).
- Cilium bpf.vlanBypass [30, 11]: radius0 is address-less like ens19, so
  its tagged frames meet the VLAN filter on uplink (#592).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:05:23 +01:00
8 changed files with 33 additions and 102 deletions
+4 -27
View File
@@ -24,33 +24,10 @@ paths.
The anonymous outer identity is mandatory for authentik clients: without it
the login goes to the UDM and fails.
UDM target: home server `udm`, `10.2.11.1` UDP 1812 (the UDM on VLAN 11,
UniFi network "(11) RADIUS"), shared secret `UDM_RADIUS_SECRET` from OpenBao
`apps/radius`. The UDM RADIUS accepts clients from `10.2.11.0/24` only, not
from the pod CIDR.
### Path to the UDM
Each FreeRADIUS pod has a second interface `net1` on VLAN 11
(NetworkAttachmentDefinition `freeradius-udm`,
`kubernetes/apps/authentik/freeradius-network-attachment.yaml`):
- macvlan on the node link `radius0` (VLAN 11 on `uplink`, no host addresses;
VLANConfig in `kubernetes/talos/patches/k8s-{1,2,3}.yaml`). VLAN 11 is in
Cilium's `bpf.vlanBypass`, otherwise the tagged frames are dropped on
`uplink`.
- IPv4 leased from the UDM DHCP server (cni-dhcp-daemon), requested without
the routers option: `net1` holds only the connected `10.2.11.0/24`, no
default route. The proxy socket is not bound to an address; the kernel's
route lookup for `10.2.11.1` picks `net1` and its leased address as source.
- IPv6 disabled on `net1` (VLAN 11 sends router advertisements).
- The NAS listeners are bound to `eth0` (`interface = eth0`); nothing listens
on `net1`.
- Traffic on `net1` bypasses Cilium; there is no CNP for the UDM hop.
`radius0` must exist on every node before FreeRADIUS pods are scheduled
there: without it the macvlan ADD fails and the pod stays in
`ContainerCreating`.
UDM target: home server `udm`, `10.2.47.1` UDP 1812 (the UDM on VLAN 47, same
address as the BGP peer in `kubernetes/infrastructure/post-crds/cilium-bgp.yaml`),
shared secret `UDM_RADIUS_SECRET` from OpenBao `apps/radius`. Egress is
allowed by CNP `freeradius-udm-egress` in `freeradius-cnp.yaml`.
## authentik objects
@@ -23,6 +23,26 @@ spec:
- port: "1813"
protocol: UDP
---
# FreeRADIUS proxies UniFi Endpoint logins to the UDM RADIUS on VLAN 47.
# 10.2.47.1 is also the UDM BGP peer in
# kubernetes/infrastructure/post-crds/cilium-bgp.yaml.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: freeradius-udm-egress
namespace: authentik
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: freeradius
egress:
- toCIDR:
- 10.2.47.1/32
toPorts:
- ports:
- port: "1812"
protocol: UDP
---
# FreeRADIUS proxies inner PAP to the 802.1X outpost on its container port.
# Additive: default-deny-ingress already admits the whole namespace.
apiVersion: cilium.io/v2
@@ -156,12 +156,11 @@ data:
}
# UDM RADIUS (FreeRADIUS) for UniFi Endpoint accounts; gets the whole
# outer EAP conversation. Address: UDM on VLAN 11 "(11) RADIUS", reached
# over net1 (freeradius-network-attachment.yaml); the UDM accepts
# RADIUS clients from 10.2.11.0/24 only.
# outer EAP conversation. Address: UDM on VLAN 47, also the BGP peer in
# kubernetes/infrastructure/post-crds/cilium-bgp.yaml.
home_server udm {
type = auth
ipaddr = 10.2.11.1
ipaddr = 10.2.47.1
port = 1812
proto = udp
secret = $ENV{UDM_RADIUS_SECRET}
@@ -233,34 +232,28 @@ data:
sites-enabled_default: |
server default {
# NAS listeners on eth0 only; net1 (VLAN 11) carries the UDM proxy
# traffic and nothing else.
listen {
type = auth
ipaddr = *
port = 1812
interface = eth0
}
listen {
type = acct
ipaddr = *
port = 1813
interface = eth0
}
listen {
type = auth
ipv6addr = ::
port = 1812
interface = eth0
}
listen {
type = acct
ipv6addr = ::
port = 1813
interface = eth0
}
authorize {
@@ -1,48 +0,0 @@
---
# FreeRADIUS net1: macvlan on radius0, VLAN 11 "(11) RADIUS" (VLANConfig in
# kubernetes/talos/patches/k8s-{1,2,3}.yaml). Carries only the proxy traffic
# to home server `udm` (freeradius-config.yaml): the UDM RADIUS accepts
# clients from 10.2.11.0/24, not from the pod CIDR. Bypasses Cilium policy.
# Address leased from the UDM; nothing references it.
#
# net1 holds only its connected /24, so the unbound proxy socket reaches the
# UDM via net1 with the leased source. No sbr: it moves that route into a
# source-based table, and the proxy would leave via eth0 from the pod IP.
# skipDefault + subnet-mask: the lease is requested without routers, so no
# default route lands on net1 (dnsmasq sends only requested options).
# IPv6 off on net1: VLAN 11 sends RAs (default route, SLAAC); the UDM
# RADIUS clients are IPv4 only.
#
# Requires cni-dhcp-daemon (infrastructure/multus) and VLAN 11 in Cilium's
# bpf.vlanBypass (infrastructure/cilium/helmrelease.yaml).
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
name: freeradius-udm
namespace: authentik
spec:
config: |
{
"cniVersion": "0.3.1",
"name": "freeradius-udm",
"plugins": [
{
"type": "macvlan",
"master": "radius0",
"mode": "bridge",
"ipam": {
"type": "dhcp",
"request": [
{ "skipDefault": true },
{ "option": "subnet-mask" }
]
}
},
{
"type": "tuning",
"sysctl": {
"net.ipv6.conf.IFNAME.disable_ipv6": "1"
}
}
]
}
@@ -18,9 +18,6 @@ spec:
metadata:
labels:
app.kubernetes.io/name: freeradius
annotations:
# net1 on VLAN 11: the path to the UDM RADIUS.
k8s.v1.cni.cncf.io/networks: freeradius-udm
spec:
enableServiceLinks: false
affinity:
@@ -13,7 +13,6 @@ resources:
- radius-cert.yaml
- radius-externalsecret.yaml
- freeradius-config.yaml
- freeradius-network-attachment.yaml
- freeradius.yaml
- freeradius-cnp.yaml
# ak-pg CNPG Postgres backend (moved here from the retired seed-authentik KS;
+2 -5
View File
@@ -31,11 +31,8 @@ Pieces:
- `mock_outpost.py`: stand-in outpost on UDP 1812, reachable under the
outpost Service FQDN through a Docker network alias.
- `mock_udm.py`: stand-in UDM RADIUS at the `udm` home server address from
`proxy.conf` (`10.2.11.1`), on a second internal bridge `radtest-udm` (that
address's /24, no host address). FreeRADIUS gets a second interface there,
as `net1` in the pod: only the connected /24, no default route, and the
proxy socket picks it by route lookup. The macvlan/DHCP attachment itself
is not tested. Answers round 1 with Challenge + `State` +
`proxy.conf`, on a second internal bridge `radtest-udm` (that address's
/24, no host address). Answers round 1 with Challenge + `State` +
EAP-TTLS Start (or Reject for `endpoint-reject`), round 2 with Accept +
EAP-Success; terminates no EAP.
- `eapol.Dockerfile`: `eapol_test` from Debian `eapoltest`.
+4 -8
View File
@@ -183,14 +183,14 @@ start() {
# Same restrictions as the pod.
mount_opts freeradius
local run_opts=("${pod_opts[@]}" "${secret_env[@]}" "${mounts[@]}")
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 --network "$net" "${run_opts[@]}" "$image" radiusd -XC -d "$confdir" >"$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
@@ -199,12 +199,8 @@ start() {
exit 1
fi
# Interface names as in the pod: eth0 the NAS side (the listeners bind to
# it), net1 the UDM side. Docker attaches networks in no fixed order.
docker create --name radtest-freeradius "${run_opts[@]}" \
--network "name=$net,ip=$ip_radius,ip6=$ip6_radius,driver-opt=com.docker.network.endpoint.ifname=eth0" \
"$image" "${args[@]}" >/dev/null
docker network connect --driver-opt com.docker.network.endpoint.ifname=net1 "$net_udm" radtest-freeradius
docker create --name radtest-freeradius --ip "$ip_radius" --ip6 "$ip6_radius" "${run_opts[@]}" "$image" "${args[@]}" >/dev/null
docker network connect "$net_udm" radtest-freeradius
docker start radtest-freeradius >/dev/null
sleep 2
if [[ "$(docker inspect -f '{{.State.Running}}' radtest-freeradius)" != true ]]; then