This repo deploys a standalone FreeRADIUS server on Google Cloud (GCE) via Terraform, providing RADIUS/802.1X authentication for Ubiquiti UniFi WiFi using certificate-based EAP-TLS.
- EAP-TLS only — no passwords. MacBooks present Okta SCEP certificates (enrolled via Jamf). FreeRADIUS validates them against the Okta Intermediate CA.
- Two CA chains: Server cert signed by a self-signed RADIUS CA (org name from
server_cert_orgvariable). Client certs signed by Okta Intermediate CA. These are independent. - All secrets in GCP Secret Manager — RADIUS shared secrets, Okta CA cert, Datadog API key, and all server certificates. No secrets in instance metadata or on disk at rest.
- Server certs persist across VM replacements — generated on first boot, stored in Secret Manager, restored on subsequent boots.
- Terraform (~> 5.0 google provider) — infrastructure as code
- GCP — Compute Engine, Secret Manager, VPC, IAP
- FreeRADIUS 3.x — RADIUS server (Debian 12 package)
- MariaDB — RADIUS accounting (
radaccttable) via FreeRADIUS native SQL module - Debian 12 — VM OS
- Ubiquiti UniFi — WiFi access points (RADIUS clients)
- Okta — Identity provider (SCEP certificates via Managed Attestation)
- Jamf — MDM (enrolls SCEP certs, deploys WiFi profiles)
- Datadog — Infrastructure monitoring, log shipping to SIEM, FreeRADIUS metrics via Prometheus exporter
- freeradius_exporter — Prometheus exporter for FreeRADIUS status metrics
main.tf— Provider, GCP project creation, API enablement, Secret Manager resourcesvariables.tf— All input variables with defaultsnetwork.tf— VPC, subnet, static IP, firewall rulescompute.tf— Service account, IAM bindings, GCE instance definitionoutputs.tf— Deployment outputs (IP, SSH command, RADIUS config)datadog.tf— Optional Datadog dashboard (Terraform-managed, requiresdatadog_app_key)datadog-dashboard.json— Static JSON export of dashboard (importable via Datadog UI)scripts/startup.sh— Idempotent bootstrap: installs FreeRADIUS + MariaDB, configures EAP-TLS, manages certs via Secret Manager
terraform init # Initialize providers
terraform fmt # Format .tf files
terraform validate # Syntax/logic check
terraform plan # Preview changes
terraform apply # Deploy
terraform output # Show outputs (IP, SSH command, etc.)- The startup script (
scripts/startup.sh) uses Terraformtemplatefile()for variable injection. Shell variables that should NOT be interpolated by Terraform use$$escaping (e.g.,$${office}). - The provider block intentionally does NOT set
project— every resource setsproject = google_project.this.project_idexplicitly to avoid a circular dependency (the project is created by Terraform). - Firewall rules use
target_tags = ["radius-server"]which matches the GCE instance'stags. - FreeRADIUS config paths:
/etc/freeradius/3.0/(standard Debian location). Certs in/etc/freeradius/3.0/certs/. - In the json_log linelog module, FreeRADIUS
%{...}must be escaped as%%{...}in the Terraform template (since%{...}is Terraform template directive syntax).
server_cert_cn— RADIUS server certificate CN (must match Jamf WiFi profile)server_cert_org— Organization name used in CA and server cert subjectsradius_clients— Map of offices with CIDRs and descriptionsdatadog_app_key— Datadog Application key (enables Terraform-managed dashboard; empty = skip)
- Defined in
datadog.tfusingdatadog_dashboard_jsonresource, gated bycount = local.datadog_enabled ? 1 : 0 - Dashboard JSON is built from
local.dashboard_json(HCL map) then encoded viajsonencode() - Static export in
datadog-dashboard.json— regenerate with:echo 'jsonencode(local.dashboard_json)' | terraform console 2>/dev/null | python3 -c 'import sys,json; raw=sys.stdin.read().strip(); data=json.loads(json.loads(raw)); print(json.dumps(data, indent=2))' > datadog-dashboard.json - Template variables:
$site(filters by@site_namelog facet) and$host(filters metrics + logs by host) - Metric queries use
{$host}filter; FreeRADIUS counter metrics need.countsuffix (Datadog OpenMetrics appends it automatically to Prometheus counters) - Log queries filter with
host:$host.value @site_name:$site.value - Log-based widgets require facets declared in Datadog UI (see README for full list) — Terraform provider does not support facet creation