Skip to content

Commit 3843414

Browse files
committed
feat: add radSecProxyHosts config to allow HAProxy PROXY protocol with FreeRADIUS
FreeRADIUS performs client lookup by raw TCP source IP before reading the PROXY protocol header, so the HAProxy host IP must be in clients.conf for connections to be accepted. Adds PINT_RADIUS_RADSEC_PROXY_HOSTS (comma-separated IPs/CIDRs) which generates pint_proxy_N client blocks alongside regular RadSec clients, enabling the two-phase client lookup FreeRADIUS requires.
1 parent 6fed0a7 commit 3843414

9 files changed

Lines changed: 72 additions & 15 deletions

File tree

chart/templates/deployment.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ spec:
7878
- name: PINT_RADIUS_RADSEC_PROXY_PROTOCOL
7979
value: "true"
8080
{{- end }}
81+
{{- if .Values.config.radSecProxyHosts }}
82+
- name: PINT_RADIUS_RADSEC_PROXY_HOSTS
83+
value: {{ .Values.config.radSecProxyHosts | join "," | quote }}
84+
{{- end }}
8185
{{- if .Values.config.radSecStatusPort }}
8286
- name: PINT_RADIUS_STATUS_PORT
8387
value: {{ .Values.config.radSecStatusPort | quote }}

chart/values.schema.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@
9696
"description": "Enable PROXY protocol on the RadSec listener. Required when HAProxy fronts FreeRADIUS.",
9797
"default": false
9898
},
99+
"radSecProxyHosts": {
100+
"type": "array",
101+
"description": "IPs/CIDRs of trusted proxy hosts (e.g. HAProxy). Required alongside radSecProxyProtocol so FreeRADIUS accepts their TCP connections before reading the PROXY header.",
102+
"items": {
103+
"type": "string"
104+
},
105+
"default": []
106+
},
99107
"radSecStatusPort": {
100108
"type": "string",
101109
"description": "Override the FreeRADIUS status server port (default: 18121)."

chart/values.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ config:
3636
# RADIUS / RadSec
3737
radSecCheckCRL: true # set false to disable CRL checking in the RadSec TLS listener
3838
radSecProxyProtocol: true # set false when HAProxy is not fronting FreeRADIUS
39+
radSecProxyHosts: [] # IPs/CIDRs of trusted proxy hosts (e.g. HAProxy); required alongside radSecProxyProtocol so FreeRADIUS accepts connections before reading the PROXY header
3940
radSecStatusPort: "" # overrides the FreeRADIUS status server port (default: 18121)
4041
radSecStatusAddr: "" # overrides the status server address (host:port); useful in dev when pod IPs are unreachable
4142

internal/config/config.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ type Config struct {
4242
// FreeRADIUS status virtual server
4343
RADIUSStatusPort string // PINT_RADIUS_STATUS_PORT: port for the FreeRADIUS status virtual server
4444
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)
45-
RadSecCheckCRL bool // PINT_RADIUS_RADSEC_CHECK_CRL: enable CRL checking in the RadSec TLS listener (default true; set false for local dev)
46-
RadSecProxyProtocol bool // PINT_RADIUS_RADSEC_PROXY_PROTOCOL: expect HAProxy PROXY protocol header on RadSec connections (default false)
45+
RadSecCheckCRL bool // PINT_RADIUS_RADSEC_CHECK_CRL: enable CRL checking in the RadSec TLS listener (default true; set false for local dev)
46+
RadSecProxyProtocol bool // PINT_RADIUS_RADSEC_PROXY_PROTOCOL: expect HAProxy PROXY protocol header on RadSec connections (default false)
47+
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
4748

4849

4950
// Apple profile signing
@@ -127,6 +128,13 @@ func Load() (*Config, error) {
127128
cfg.IPASkipTLSVerify = os.Getenv("PINT_IPA_SKIP_TLS_VERIFY") == "true"
128129
cfg.RadSecCheckCRL = os.Getenv("PINT_RADIUS_RADSEC_CHECK_CRL") != "false"
129130
cfg.RadSecProxyProtocol = os.Getenv("PINT_RADIUS_RADSEC_PROXY_PROTOCOL") == "true"
131+
if v := os.Getenv("PINT_RADIUS_RADSEC_PROXY_HOSTS"); v != "" {
132+
for _, h := range strings.Split(v, ",") {
133+
if h = strings.TrimSpace(h); h != "" {
134+
cfg.RadSecProxyHosts = append(cfg.RadSecProxyHosts, h)
135+
}
136+
}
137+
}
130138
cfg.RootCAName = os.Getenv("PINT_IPA_ROOT_CA_NAME")
131139
if cfg.RootCAName == "" {
132140
cfg.RootCAName = "ipa"

internal/handlers/radius.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ func (s *Server) commitStore(c *gin.Context, store *radius.ClientStore) error {
195195
s.fail(c, http.StatusInternalServerError, "radius store save failed", err)
196196
return err
197197
}
198-
if err := radius.WriteRadiusConfig(ctx, s.K8s, s.Cfg.Namespace, s.Cfg.ConfigSecret, s.Cfg.FreeRADIUSDeployment, store.All()); err != nil {
198+
if err := radius.WriteRadiusConfig(ctx, s.K8s, s.Cfg.Namespace, s.Cfg.ConfigSecret, s.Cfg.FreeRADIUSDeployment, store.All(), s.Cfg.RadSecProxyHosts); err != nil {
199199
s.fail(c, http.StatusInternalServerError, "radius config write failed", err)
200200
return err
201201
}

internal/radius/config.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ func RenderRadSecTLS(checkCRL, proxyProtocol bool) string {
3939
`, proxy, crl)
4040
}
4141

42-
// RenderClientsConf generates the FreeRADIUS clients.conf content from the given client list.
43-
// Each client block includes proto = tls for RadSec (RFC 6614) support.
44-
func RenderClientsConf(clients []RadiusClient) string {
42+
// RenderClientsConf generates the FreeRADIUS clients.conf content from the given client list
43+
// and optional proxy host IPs/CIDRs. Each client block includes proto = tls for RadSec
44+
// (RFC 6614) support. Proxy hosts are added as bare TLS clients so FreeRADIUS accepts their
45+
// TCP connections before reading the HAProxy PROXY protocol header.
46+
func RenderClientsConf(clients []RadiusClient, proxyHosts []string) string {
4547
var b strings.Builder
4648
b.WriteString("# Auto-generated by PINT - do not edit manually\n\n")
4749

@@ -61,5 +63,16 @@ func RenderClientsConf(clients []RadiusClient) string {
6163
b.WriteString(" virtual_server = radsec\n")
6264
b.WriteString("}\n\n")
6365
}
66+
67+
for i, host := range proxyHosts {
68+
fmt.Fprintf(&b, "client pint_proxy_%d {\n", i)
69+
fmt.Fprintf(&b, " ipaddr = %s\n", host)
70+
b.WriteString(" secret = radsec\n")
71+
b.WriteString(" proto = tls\n")
72+
fmt.Fprintf(&b, " shortname = pint-proxy-%d\n", i)
73+
b.WriteString(" virtual_server = radsec\n")
74+
b.WriteString("}\n\n")
75+
}
76+
6477
return b.String()
6578
}

internal/radius/config_test.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func TestRenderClientsConf_WithIP(t *testing.T) {
5050
clients := []radius.RadiusClient{
5151
{Username: "mbillow", IPCIDR: &ip},
5252
}
53-
out := radius.RenderClientsConf(clients)
53+
out := radius.RenderClientsConf(clients, nil)
5454

5555
if !strings.Contains(out, "client mbillow_home") {
5656
t.Error("missing client block header")
@@ -76,7 +76,7 @@ func TestRenderClientsConf_NoIP(t *testing.T) {
7676
clients := []radius.RadiusClient{
7777
{Username: "jsmith", IPCIDR: nil},
7878
}
79-
out := radius.RenderClientsConf(clients)
79+
out := radius.RenderClientsConf(clients, nil)
8080

8181
if !strings.Contains(out, "ipaddr = 0.0.0.0/0") {
8282
t.Error("missing wildcard ipaddr for nil IPCIDR")
@@ -89,7 +89,7 @@ func TestRenderClientsConf_MultipleClients(t *testing.T) {
8989
{Username: "alice", IPCIDR: &ip},
9090
{Username: "bob", IPCIDR: nil},
9191
}
92-
out := radius.RenderClientsConf(clients)
92+
out := radius.RenderClientsConf(clients, nil)
9393

9494
if !strings.Contains(out, "client alice_home") {
9595
t.Error("missing alice block")
@@ -100,8 +100,31 @@ func TestRenderClientsConf_MultipleClients(t *testing.T) {
100100
}
101101

102102
func TestRenderClientsConf_Empty(t *testing.T) {
103-
out := radius.RenderClientsConf(nil)
103+
out := radius.RenderClientsConf(nil, nil)
104104
if !strings.Contains(out, "Auto-generated by PINT") {
105105
t.Error("missing header comment")
106106
}
107107
}
108+
109+
func TestRenderClientsConf_ProxyHosts(t *testing.T) {
110+
out := radius.RenderClientsConf(nil, []string{"10.0.0.1", "10.0.0.2/32"})
111+
112+
if !strings.Contains(out, "client pint_proxy_0") {
113+
t.Error("missing pint_proxy_0 block")
114+
}
115+
if !strings.Contains(out, "ipaddr = 10.0.0.1") {
116+
t.Error("missing first proxy host IP")
117+
}
118+
if !strings.Contains(out, "client pint_proxy_1") {
119+
t.Error("missing pint_proxy_1 block")
120+
}
121+
if !strings.Contains(out, "ipaddr = 10.0.0.2/32") {
122+
t.Error("missing second proxy host IP")
123+
}
124+
if !strings.Contains(out, "proto = tls") {
125+
t.Error("missing proto = tls on proxy client")
126+
}
127+
if !strings.Contains(out, "virtual_server = radsec") {
128+
t.Error("missing virtual_server = radsec on proxy client")
129+
}
130+
}

internal/radius/reload.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ const (
2222
KeyRadSecTLS = "radsec-tls.conf"
2323
)
2424

25-
// WriteRadiusConfig renders clients.conf from the given client list, patches the
25+
// WriteRadiusConfig renders clients.conf from the given client list and proxy hosts, patches the
2626
// key in the named Kubernetes Secret, and triggers a FreeRADIUS rollout restart.
27-
func WriteRadiusConfig(ctx context.Context, k8s kubernetes.Interface, namespace, secretName, deployment string, clients []RadiusClient) error {
28-
if err := patchSecretKey(ctx, k8s, namespace, secretName, KeyClientsConf, []byte(RenderClientsConf(clients))); err != nil {
27+
func WriteRadiusConfig(ctx context.Context, k8s kubernetes.Interface, namespace, secretName, deployment string, clients []RadiusClient, proxyHosts []string) error {
28+
if err := patchSecretKey(ctx, k8s, namespace, secretName, KeyClientsConf, []byte(RenderClientsConf(clients, proxyHosts))); err != nil {
2929
return err
3030
}
3131
return Reload(ctx, k8s, namespace, deployment)
@@ -89,7 +89,7 @@ func EnsureConfigSecret(ctx context.Context, k8s kubernetes.Interface, namespace
8989
ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: namespace},
9090
Data: map[string][]byte{
9191
KeyClientsJSON: []byte("[]"),
92-
KeyClientsConf: []byte(RenderClientsConf(nil)),
92+
KeyClientsConf: []byte(RenderClientsConf(nil, nil)),
9393
KeyStatusSecret: []byte(""),
9494
KeyStatus: []byte(""),
9595
KeyRadSecTLS: []byte(RenderRadSecTLS(true, false)),

internal/radius/reload_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func TestWriteRadiusConfig(t *testing.T) {
3434
{Username: "mbillow", IPCIDR: nil},
3535
}
3636

37-
if err := radius.WriteRadiusConfig(ctx, k8s, "default", "pint-config", "", clients); err != nil {
37+
if err := radius.WriteRadiusConfig(ctx, k8s, "default", "pint-config", "", clients, nil); err != nil {
3838
t.Fatalf("WriteRadiusConfig() error: %v", err)
3939
}
4040

0 commit comments

Comments
 (0)