diff --git a/chart/Chart.yaml b/chart/Chart.yaml index 3170a14..0ceeb09 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: pint description: Pouring IPA for Network Trust - CSH WiFi EAP-TLS enrollment and home RadSec management type: application -version: 0.3.3 +version: 0.4.0 appVersion: "0.1.0" diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index 9910c27..9b7e135 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -78,6 +78,10 @@ spec: - name: PINT_RADIUS_RADSEC_PROXY_PROTOCOL value: "true" {{- end }} + {{- if .Values.config.radSecProxyHosts }} + - name: PINT_RADIUS_RADSEC_PROXY_HOSTS + value: {{ .Values.config.radSecProxyHosts | join "," | quote }} + {{- end }} {{- if .Values.config.radSecStatusPort }} - name: PINT_RADIUS_STATUS_PORT value: {{ .Values.config.radSecStatusPort | quote }} diff --git a/chart/values.schema.json b/chart/values.schema.json index c06a3dc..4b7dd7a 100644 --- a/chart/values.schema.json +++ b/chart/values.schema.json @@ -96,6 +96,14 @@ "description": "Enable PROXY protocol on the RadSec listener. Required when HAProxy fronts FreeRADIUS.", "default": false }, + "radSecProxyHosts": { + "type": "array", + "description": "IPs/CIDRs of trusted proxy hosts (e.g. HAProxy). Required alongside radSecProxyProtocol so FreeRADIUS accepts their TCP connections before reading the PROXY header.", + "items": { + "type": "string" + }, + "default": [] + }, "radSecStatusPort": { "type": "string", "description": "Override the FreeRADIUS status server port (default: 18121)." diff --git a/chart/values.yaml b/chart/values.yaml index 2d3384d..e5800de 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -36,6 +36,7 @@ config: # RADIUS / RadSec radSecCheckCRL: true # set false to disable CRL checking in the RadSec TLS listener radSecProxyProtocol: true # set false when HAProxy is not fronting FreeRADIUS + radSecProxyHosts: [] # IPs/CIDRs of trusted proxy hosts (e.g. HAProxy); required alongside radSecProxyProtocol so FreeRADIUS accepts connections before reading the PROXY header radSecStatusPort: "" # overrides the FreeRADIUS status server port (default: 18121) radSecStatusAddr: "" # overrides the status server address (host:port); useful in dev when pod IPs are unreachable diff --git a/cmd/pint/main.go b/cmd/pint/main.go index 5293633..63f3a63 100644 --- a/cmd/pint/main.go +++ b/cmd/pint/main.go @@ -96,6 +96,13 @@ func main() { if err := radius.WriteRadSecTLS(ctx, k8sClient, cfg.Namespace, cfg.ConfigSecret, cfg.FreeRADIUSDeployment, cfg.RadSecCheckCRL, cfg.RadSecProxyProtocol); err != nil { log.Fatal("write radsec-tls.conf failed", zap.Error(err)) } + clientStore := radius.NewClientStore(k8sClient, cfg.Namespace, cfg.ConfigSecret) + if err := clientStore.Load(ctx); err != nil { + log.Fatal("load radius client store failed", zap.Error(err)) + } + if err := radius.WriteRadiusConfig(ctx, k8sClient, cfg.Namespace, cfg.ConfigSecret, cfg.FreeRADIUSDeployment, clientStore.All(), cfg.RadSecProxyHosts); err != nil { + log.Fatal("write clients.conf failed", zap.Error(err)) + } // Fetch CA certs in parallel. var ( diff --git a/internal/config/config.go b/internal/config/config.go index 9a020da..7002da1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -42,8 +42,9 @@ type Config struct { // FreeRADIUS status virtual server RADIUSStatusPort string // PINT_RADIUS_STATUS_PORT: port for the FreeRADIUS status virtual server RADIUSStatusAddr string // PINT_RADIUS_STATUS_ADDR: override address (host:port) for status queries; replaces per-pod IP (useful when pod IPs are unreachable, e.g. local dev against kind) - RadSecCheckCRL bool // PINT_RADIUS_RADSEC_CHECK_CRL: enable CRL checking in the RadSec TLS listener (default true; set false for local dev) - RadSecProxyProtocol bool // PINT_RADIUS_RADSEC_PROXY_PROTOCOL: expect HAProxy PROXY protocol header on RadSec connections (default false) + RadSecCheckCRL bool // PINT_RADIUS_RADSEC_CHECK_CRL: enable CRL checking in the RadSec TLS listener (default true; set false for local dev) + RadSecProxyProtocol bool // PINT_RADIUS_RADSEC_PROXY_PROTOCOL: expect HAProxy PROXY protocol header on RadSec connections (default false) + RadSecProxyHosts []string // PINT_RADIUS_RADSEC_PROXY_HOSTS: comma-separated IPs/CIDRs of trusted proxy hosts (e.g. HAProxy); added as clients so FreeRADIUS accepts their connections before reading the PROXY header // Apple profile signing @@ -127,6 +128,13 @@ func Load() (*Config, error) { cfg.IPASkipTLSVerify = os.Getenv("PINT_IPA_SKIP_TLS_VERIFY") == "true" cfg.RadSecCheckCRL = os.Getenv("PINT_RADIUS_RADSEC_CHECK_CRL") != "false" cfg.RadSecProxyProtocol = os.Getenv("PINT_RADIUS_RADSEC_PROXY_PROTOCOL") == "true" + if v := os.Getenv("PINT_RADIUS_RADSEC_PROXY_HOSTS"); v != "" { + for _, h := range strings.Split(v, ",") { + if h = strings.TrimSpace(h); h != "" { + cfg.RadSecProxyHosts = append(cfg.RadSecProxyHosts, h) + } + } + } cfg.RootCAName = os.Getenv("PINT_IPA_ROOT_CA_NAME") if cfg.RootCAName == "" { cfg.RootCAName = "ipa" diff --git a/internal/handlers/radius.go b/internal/handlers/radius.go index 7d891f0..bd35ae4 100644 --- a/internal/handlers/radius.go +++ b/internal/handlers/radius.go @@ -195,7 +195,7 @@ func (s *Server) commitStore(c *gin.Context, store *radius.ClientStore) error { s.fail(c, http.StatusInternalServerError, "radius store save failed", err) return err } - if err := radius.WriteRadiusConfig(ctx, s.K8s, s.Cfg.Namespace, s.Cfg.ConfigSecret, s.Cfg.FreeRADIUSDeployment, store.All()); err != nil { + if err := radius.WriteRadiusConfig(ctx, s.K8s, s.Cfg.Namespace, s.Cfg.ConfigSecret, s.Cfg.FreeRADIUSDeployment, store.All(), s.Cfg.RadSecProxyHosts); err != nil { s.fail(c, http.StatusInternalServerError, "radius config write failed", err) return err } diff --git a/internal/radius/config.go b/internal/radius/config.go index 30c0ecc..99a9f67 100644 --- a/internal/radius/config.go +++ b/internal/radius/config.go @@ -39,9 +39,11 @@ func RenderRadSecTLS(checkCRL, proxyProtocol bool) string { `, proxy, crl) } -// RenderClientsConf generates the FreeRADIUS clients.conf content from the given client list. -// Each client block includes proto = tls for RadSec (RFC 6614) support. -func RenderClientsConf(clients []RadiusClient) string { +// RenderClientsConf generates the FreeRADIUS clients.conf content from the given client list +// and optional proxy host IPs/CIDRs. Each client block includes proto = tls for RadSec +// (RFC 6614) support. Proxy hosts are added as bare TLS clients so FreeRADIUS accepts their +// TCP connections before reading the HAProxy PROXY protocol header. +func RenderClientsConf(clients []RadiusClient, proxyHosts []string) string { var b strings.Builder b.WriteString("# Auto-generated by PINT - do not edit manually\n\n") @@ -61,5 +63,16 @@ func RenderClientsConf(clients []RadiusClient) string { b.WriteString(" virtual_server = radsec\n") b.WriteString("}\n\n") } + + for i, host := range proxyHosts { + fmt.Fprintf(&b, "client pint_proxy_%d {\n", i) + fmt.Fprintf(&b, " ipaddr = %s\n", host) + b.WriteString(" secret = radsec\n") + b.WriteString(" proto = tls\n") + fmt.Fprintf(&b, " shortname = pint-proxy-%d\n", i) + b.WriteString(" virtual_server = radsec\n") + b.WriteString("}\n\n") + } + return b.String() } diff --git a/internal/radius/config_test.go b/internal/radius/config_test.go index 6c68fe9..d64fd3c 100644 --- a/internal/radius/config_test.go +++ b/internal/radius/config_test.go @@ -50,7 +50,7 @@ func TestRenderClientsConf_WithIP(t *testing.T) { clients := []radius.RadiusClient{ {Username: "mbillow", IPCIDR: &ip}, } - out := radius.RenderClientsConf(clients) + out := radius.RenderClientsConf(clients, nil) if !strings.Contains(out, "client mbillow_home") { t.Error("missing client block header") @@ -76,7 +76,7 @@ func TestRenderClientsConf_NoIP(t *testing.T) { clients := []radius.RadiusClient{ {Username: "jsmith", IPCIDR: nil}, } - out := radius.RenderClientsConf(clients) + out := radius.RenderClientsConf(clients, nil) if !strings.Contains(out, "ipaddr = 0.0.0.0/0") { t.Error("missing wildcard ipaddr for nil IPCIDR") @@ -89,7 +89,7 @@ func TestRenderClientsConf_MultipleClients(t *testing.T) { {Username: "alice", IPCIDR: &ip}, {Username: "bob", IPCIDR: nil}, } - out := radius.RenderClientsConf(clients) + out := radius.RenderClientsConf(clients, nil) if !strings.Contains(out, "client alice_home") { t.Error("missing alice block") @@ -100,8 +100,31 @@ func TestRenderClientsConf_MultipleClients(t *testing.T) { } func TestRenderClientsConf_Empty(t *testing.T) { - out := radius.RenderClientsConf(nil) + out := radius.RenderClientsConf(nil, nil) if !strings.Contains(out, "Auto-generated by PINT") { t.Error("missing header comment") } } + +func TestRenderClientsConf_ProxyHosts(t *testing.T) { + out := radius.RenderClientsConf(nil, []string{"10.0.0.1", "10.0.0.2/32"}) + + if !strings.Contains(out, "client pint_proxy_0") { + t.Error("missing pint_proxy_0 block") + } + if !strings.Contains(out, "ipaddr = 10.0.0.1") { + t.Error("missing first proxy host IP") + } + if !strings.Contains(out, "client pint_proxy_1") { + t.Error("missing pint_proxy_1 block") + } + if !strings.Contains(out, "ipaddr = 10.0.0.2/32") { + t.Error("missing second proxy host IP") + } + if !strings.Contains(out, "proto = tls") { + t.Error("missing proto = tls on proxy client") + } + if !strings.Contains(out, "virtual_server = radsec") { + t.Error("missing virtual_server = radsec on proxy client") + } +} diff --git a/internal/radius/reload.go b/internal/radius/reload.go index 1a04305..6cb1bf9 100644 --- a/internal/radius/reload.go +++ b/internal/radius/reload.go @@ -22,10 +22,10 @@ const ( KeyRadSecTLS = "radsec-tls.conf" ) -// WriteRadiusConfig renders clients.conf from the given client list, patches the +// WriteRadiusConfig renders clients.conf from the given client list and proxy hosts, patches the // key in the named Kubernetes Secret, and triggers a FreeRADIUS rollout restart. -func WriteRadiusConfig(ctx context.Context, k8s kubernetes.Interface, namespace, secretName, deployment string, clients []RadiusClient) error { - if err := patchSecretKey(ctx, k8s, namespace, secretName, KeyClientsConf, []byte(RenderClientsConf(clients))); err != nil { +func WriteRadiusConfig(ctx context.Context, k8s kubernetes.Interface, namespace, secretName, deployment string, clients []RadiusClient, proxyHosts []string) error { + if err := patchSecretKey(ctx, k8s, namespace, secretName, KeyClientsConf, []byte(RenderClientsConf(clients, proxyHosts))); err != nil { return err } return Reload(ctx, k8s, namespace, deployment) @@ -89,7 +89,7 @@ func EnsureConfigSecret(ctx context.Context, k8s kubernetes.Interface, namespace ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: namespace}, Data: map[string][]byte{ KeyClientsJSON: []byte("[]"), - KeyClientsConf: []byte(RenderClientsConf(nil)), + KeyClientsConf: []byte(RenderClientsConf(nil, nil)), KeyStatusSecret: []byte(""), KeyStatus: []byte(""), KeyRadSecTLS: []byte(RenderRadSecTLS(true, false)), diff --git a/internal/radius/reload_test.go b/internal/radius/reload_test.go index f662a59..e1b6342 100644 --- a/internal/radius/reload_test.go +++ b/internal/radius/reload_test.go @@ -34,7 +34,7 @@ func TestWriteRadiusConfig(t *testing.T) { {Username: "mbillow", IPCIDR: nil}, } - if err := radius.WriteRadiusConfig(ctx, k8s, "default", "pint-config", "", clients); err != nil { + if err := radius.WriteRadiusConfig(ctx, k8s, "default", "pint-config", "", clients, nil); err != nil { t.Fatalf("WriteRadiusConfig() error: %v", err) }