Federation Relay¶
The Federation Relay is the only shared infrastructure in FadianRoam. It proxies RADIUS authentication requests between members.
Role¶
The Relay acts as a central RADIUS proxy:
graph LR
RA[Member A<br/>RADIUS] -->|user@realm.b| RELAY[Federation Relay<br/>FreeRADIUS Proxy]
RELAY -->|forward| RB[Member B<br/>RADIUS]
RB -->|Access-Accept| RELAY
RELAY -->|Access-Accept| RA
- Receives RADIUS requests from members over MGMT VPN
- Looks up the realm to determine the destination member
- Forwards the request to the correct member's RADIUS
- Returns the response
The Relay does not:
- Store any user credentials
- Inspect inner EAP payloads (encrypted in TLS tunnel)
- Participate in user data traffic
- Run an IDP or Keycloak instance
Architecture¶
Components¶
| Component | Purpose |
|---|---|
| FreeRADIUS | RADIUS proxy engine |
| WireGuard | MGMT VPN hub (star topology) |
| Federation config | Realm → member IP mapping |
Network¶
The Relay is the hub of the MGMT VPN star:
- IP:
172.172.10.1 - Listens on RADIUS ports 1812/1813 within the MGMT subnet
- Each member has a WireGuard peer entry on the Relay
FreeRADIUS Proxy Configuration¶
Realm Definitions¶
The Relay maintains a realm entry for each member, generated from the federation registry:
# proxy.conf on Federation Relay
# Member A
realm roam.member-a.net {
type = radius
authhost = 172.172.10.10:1812
accthost = 172.172.10.10:1813
secret = <member-a-shared-secret>
nostrip
}
# Member B
realm roam.member-b.org {
type = radius
authhost = 172.172.10.11:1812
accthost = 172.172.10.11:1813
secret = <member-b-shared-secret>
nostrip
}
# Reject unknown realms
realm DEFAULT {
reject = yes
}
Key settings:
nostrip: Preserves the fulluser@realmso the destination RADIUS can process itDEFAULTrealm rejects unknown realms (only registered members are proxied)- Each member has a unique shared secret
Client Definitions¶
Each member is defined as a RADIUS client:
# clients.conf on Federation Relay
client member-a {
ipaddr = 172.172.10.10
secret = <member-a-shared-secret>
shortname = member-a
}
client member-b {
ipaddr = 172.172.10.11
secret = <member-b-shared-secret>
shortname = member-b
}
Virtual Server¶
The Relay runs a minimal virtual server — it only proxies, no local authentication:
# sites-enabled/default on Relay
authorize {
preprocess
suffix
# No local auth — all requests are proxied via realm routing
}
authenticate {
# Empty — proxy handles authentication
}
WireGuard Hub Configuration¶
# /etc/wireguard/fadianroam-mgmt.conf on Relay
[Interface]
Address = 172.172.10.1/24
PrivateKey = <relay-private-key>
ListenPort = 51820
[Peer]
# Member A
PublicKey = <member-a-pubkey>
AllowedIPs = 172.172.10.10/32
[Peer]
# Member B
PublicKey = <member-b-pubkey>
AllowedIPs = 172.172.10.11/32
# ... one [Peer] per member
Configuration Management¶
The Relay's configuration is derived from the federation registry (members/*.yml files):
- Member submits PR with their
members/<realm>.yml - PR is reviewed and approved
- On merge, Relay configuration is regenerated:
proxy.conf: New realm entryclients.conf: New client entry- WireGuard: New peer entry
- Services are reloaded
This can be automated with CI/CD:
# Example: regenerate and reload
./scripts/generate-relay-config.sh
systemctl reload freeradius
wg syncconf fadianroam-mgmt <(wg-quick strip fadianroam-mgmt)
Deployment Recommendations¶
| Aspect | Recommendation |
|---|---|
| Server | Dedicated VPS (separate from any member's infrastructure) |
| Location | Low-latency region for majority of members |
| Specs | 1 vCPU, 1 GB RAM (lightweight proxy workload) |
| OS | Debian 12+ |
| Redundancy | Future: multiple relays with shared realm config |
Monitoring¶
RADIUS¶
# Check FreeRADIUS is running
systemctl status freeradius
# Debug mode (stop service first)
freeradius -X
# Watch proxy decisions in debug output:
# - "Found realm: roam.member-b.org"
# - "Proxying to realm roam.member-b.org"
WireGuard¶
# All peer handshakes
wg show fadianroam-mgmt latest-handshakes
# Identify members with stale handshakes (>2 minutes)
wg show fadianroam-mgmt latest-handshakes | awk '$2 > 120 {print $1}'