PINT is a self-service WiFi enrollment portal for Computer Science House. Members log in with their CSH Keycloak account and PINT issues them a certificate from FreeIPA. That certificate is used to authenticate to the WiFi network via EAP-TLS, with no passwords involved. WiFi controllers (home routers, etc.) can also enroll for a RadSec client certificate to proxy authentication back to FreeRADIUS over a mutual-TLS connection.
PINT is a single stateless Go binary. There is no database. All persistent state lives in Kubernetes Secrets that FreeRADIUS mounts directly.
flowchart TD
U(Member Device) -->|OIDC login\nprofile download| P[PINT :8080]
U -->|EAP-TLS / 802.1X| C(WiFi Controller)
C -->|RadSec mTLS\nport 2083| FR[FreeRADIUS]
P -->|cert_request\nca_show| IPA[(FreeIPA)]
P -->|write config\nwrite certs| KS[(Kubernetes\nSecrets)]
P -->|rollout restart| FR
KS -->|mounted at runtime| FR
PINT uses two distinct paths to issue WiFi client certificates depending on the platform.
iOS and macOS — SCEP (on-device key generation)
PINT acts as a SCEP Registration Authority. The device generates its own RSA 2048 keypair locally and enrolls through the /scep endpoint embedded in the mobileconfig. The private key never leaves the device, and iOS handles automatic renewal before the certificate expires.
sequenceDiagram
participant iOS as iOS / macOS Device
participant P as PINT
participant IPA as FreeIPA (Dogtag)
iOS->>P: Download profile (authenticated)
P->>P: Issue one-time SCEP challenge
P-->>iOS: Mobileconfig with SCEP payload + challenge
note over iOS: Device generates RSA 2048 keypair on-device
iOS->>P: SCEP PKIOperation (CSR + challenge, CMS-wrapped)
P->>P: Validate challenge and extract username
P->>IPA: cert_request(CSR, username, pint_eap_client)
IPA-->>P: Signed certificate (DER)
P-->>iOS: CMS-wrapped certificate response
note over iOS: Certificate installed and WiFi connects
note over iOS,P: Near expiry: iOS re-runs SCEP automatically
The SCEP Registration Authority (RA) uses a self-signed RSA 2048 certificate (CN CSH PINT SCEP RA) stored in the pint-scep-ra-cert Kubernetes Secret. PINT generates this automatically on first startup if the Secret does not exist. It is intentionally self-signed and set to never expire. The RA cert is not a trust anchor; it is only used to encrypt the CMS envelope during the SCEP exchange. iOS identifies it via the SHA-1 fingerprint embedded in the mobileconfig's SCEP payload.
All other platforms — server-side key generation
For Android, Windows, and Linux, PINT generates a secp384r1 ECDSA keypair, submits the CSR to FreeIPA's cert_request RPC, and bundles the result into a PKCS#12 archive for download. The private key is shown once and never stored.
sequenceDiagram
participant Client as Browser
participant P as PINT
participant IPA as FreeIPA (Dogtag)
Client->>P: Request profile download
P->>P: Generate secp384r1 keypair + CSR
P->>IPA: cert_request(CSR, principal, CA, profile)
IPA-->>P: Signed certificate (DER)
P->>P: Bundle into .p12 / .xml
P-->>Client: Download
PINT authenticates to FreeIPA using a service account specified by PINT_IPA_SERVICE_ACCOUNT and PINT_IPA_PASSWORD. The session is established at startup and re-authenticated automatically on 401.
Three custom Dogtag certificate profiles control validity, key usage, and subject enforcement. All profiles force O=CSH.RIT.EDU in the issued certificate subject regardless of what the CSR contains.
| Profile | Purpose | Validity | Key | EKU |
|---|---|---|---|---|
pint_eap_client |
EAP-TLS client certs for member devices | 1 year | RSA 2048 or EC | clientAuth |
pint_radsec_client |
mTLS client certs for WiFi controllers | 5 years | EC (secp384r1) | clientAuth |
pint_radsec_server |
Server cert for FreeRADIUS; used for both the outer RadSec TLS listener and the inner EAP-TLS authentication (two separate certs, same profile) | 90 days | EC (secp384r1) | serverAuth |
pint_profile_signing |
CMS signing cert for iOS mobileconfig profiles | 1 year | EC (secp384r1) | codeSigning |
The pint_eap_client profile accepts both RSA and EC keys because iOS generates RSA 2048 on-device via SCEP while other platforms submit EC keypairs generated by PINT. The 1-year validity is short enough to rotate credentials regularly while remaining transparent to users on iOS/macOS, where SCEP handles renewal automatically. The 90-day server certs and 1-year profile signing cert are automatically renewed by PINT (see RadSec Server Cert, EAP Server Cert, and Profile Signing Cert).
Profile config files live in ipa/profiles/. They must be imported into FreeIPA once before PINT can use them. Use ipa/update_profile.py, which supports three actions:
| Action | FreeIPA call | When to use |
|---|---|---|
update |
certprofile_mod |
Profile already exists; push changes |
show |
certprofile_show |
Inspect what is currently deployed |
reimport |
certprofile_del + certprofile_import |
Profile config is missing from FreeIPA (first import or after manual Dogtag changes) |
cd ipa
python3 update_profile.pyThe corresponding environment variables (all optional; defaults shown):
PINT_IPA_EAP_CLIENT_CERT_PROFILE=pint_eap_client
PINT_IPA_RADSEC_CLIENT_CERT_PROFILE=pint_radsec_client
PINT_IPA_RADSEC_SERVER_CERT_PROFILE=pint_radsec_server
PINT_IPA_CODE_SIGNING_CERT_PROFILE=pint_profile_signing
Members visit /profile and download a platform-specific package. PINT issues a fresh certificate on each download.
| Platform | Output | Contents |
|---|---|---|
| iOS / macOS | .mobileconfig (Apple Configuration Profile) |
SCEP payload, WiFi CA, root CA, code-signing CA, 802.1X/EAP-TLS config; optionally CMS-signed |
| Android | .p12 (PKCS#12) |
Client cert + key + WiFi CA, imported via Android WiFi settings |
| Windows | .xml (WLAN profile) + .p12 (PKCS#12) |
EAP-TLS config and CA thumbprint; cert imported separately into the Windows certificate store |
The iOS mobileconfig always embeds the WiFi intermediate CA and root CA so the full trust chain is installed in one step. The SCEP payload instructs iOS to generate a keypair on-device, enroll with PINT's /scep endpoint using a one-time challenge, and renew automatically near expiry — users never need to re-download the profile. When PINT_IPA_CODE_SIGNING_CA_NAME is set, PINT also embeds the code-signing intermediate CA and wraps the profile in a CMS SignedData envelope, letting iOS display it as "Verified" after the CA profile is trusted.
Members running home routers or other WiFi controllers can enroll for a RadSec client certificate. This lets their equipment proxy 802.1X authentication requests back to FreeRADIUS over a mutual-TLS connection on port 2083.
Enrollment:
- Member visits
/radius, enters their controller's source IP, and clicks Enroll. - PINT generates a secp384r1 keypair and requests a
pint_radsec_clientcertificate from FreeIPA. - The private key and certificate PEM are displayed once. PINT does not retain them.
- PINT writes an updated
clients.confto the Kubernetes config Secret and triggers a FreeRADIUS rollout restart. - The member configures their router with the cert, key, and the RadSec CA chain (downloadable from
/radius/ca). The RADIUS shared secret is alwaysradsec(standard for RFC 6614).
IP allowlist: A source IP address is required at enrollment and when updating. Regular members must supply a single bare IP; CIDR ranges are rejected. Requests arriving from any other address are dropped by FreeRADIUS before authentication begins. Only the organisation-level controller (managed via /admin/radius) accepts a CIDR range or no restriction.
Lifecycle: Members can update their IP allowlist, regenerate credentials (revokes and replaces the cert), or delete their enrollment entirely at any time from /radius. Admins (RTP group) have the same controls over any member's enrollment via /admin/radius, and can provision an organisation-level controller (root) that is not tied to any member account.
PINT manages FreeRADIUS entirely through the Kubernetes API with no direct process communication.
flowchart LR
P[PINT] -->|clients.json\nclients.conf\nradsec-tls.conf\nstatus config| CS[(pint-config\nSecret)]
P -->|tls.crt · tls.key\nca.pem| RS[(pint-radsec-server-\ncertificates Secret)]
P -->|eap.crt · eap.key\nwifi-ca.pem| ES[(pint-eap-server-\ncert Secret)]
P -->|tls.crt · tls.key| PS[(pint-profile-signing-\ncert Secret)]
P -->|patch restartedAt\nannotation| D[pint-freeradius\nDeployment]
CS -->|volume mount\n/etc/pint/config/| FR[FreeRADIUS Pod]
RS -->|volume mount\n/etc/pint/radsec/| FR
ES -->|volume mount\n/etc/pint/eap/| FR
pint-config Secret: PINT writes and owns all keys.
| Key | Description |
|---|---|
clients.json |
Enrolled controller list (PINT's source of truth) |
clients.conf |
FreeRADIUS client configuration rendered from clients.json |
radsec-tls.conf |
TLS block for the RadSec listener; CRL checking on/off via PINT_RADIUS_RADSEC_CHECK_CRL |
status |
Status virtual server client config |
status-secret |
Shared secret for status server queries |
pint-radsec-server-certificates Secret: Outer RadSec TLS material (router ↔ FreeRADIUS).
| Key | Description |
|---|---|
tls.crt / tls.key |
RadSec server certificate and private key (RadSec CA-issued) |
ca.pem |
RadSec CA chain used to verify router client certs |
pint-eap-server-cert Secret: EAP-TLS inner authentication material (iOS/device ↔ FreeRADIUS).
| Key | Description |
|---|---|
eap.crt / eap.key |
EAP-TLS server certificate and private key (Wireless CA-issued); presented to devices during 802.1X |
wifi-ca.pem |
WiFi CA chain used to verify EAP-TLS client certs presented by devices |
pint-profile-signing-cert Secret: CMS signing identity for iOS mobileconfig profiles. Only created when PINT_IPA_CODE_SIGNING_CA_NAME is set.
| Key | Description |
|---|---|
tls.crt / tls.key |
Profile signing certificate and private key |
When any config changes, PINT patches the FreeRADIUS Deployment's kubectl.kubernetes.io/restartedAt annotation, triggering a rolling restart that picks up the new Secret contents.
At startup, PINT checks whether the RadSec server certificate has more than 30 days of validity remaining. If the cert is missing or nearing expiry, PINT requests a new pint_radsec_server certificate from FreeIPA, writes it to the pint-radsec-server-certificates Secret, and triggers a FreeRADIUS restart. A background goroutine repeats this check every 24 hours, so renewals are fully automatic.
PINT manages a separate certificate for FreeRADIUS's EAP-TLS inner authentication using the same lifecycle as the RadSec cert. At startup it checks the pint-eap-server-cert Secret; if the cert is missing, nearing expiry (within 30 days), or was previously issued without a DNS SAN (required by iOS 13+), PINT requests a fresh certificate from FreeIPA using the pint_radsec_server profile (configurable via PINT_IPA_EAP_CERT_PROFILE), bundles it with the Wireless CA chain, and writes eap.crt, eap.key, and wifi-ca.pem to the Secret. A background goroutine renews the cert daily as needed.
The EAP cert must be issued by the Wireless CA (not the RadSec CA) because iOS devices anchor trust to the WiFi CA embedded in their mobileconfig profile. The cert must include a DNS SAN matching the RADIUS server hostname; iOS 13+ ignores the CN for EAP-TLS server identity verification.
When PINT_IPA_CODE_SIGNING_CA_NAME is set, PINT manages a CMS signing certificate using the same pattern as the RadSec server cert. At startup it checks the pint-profile-signing-cert Secret; if the cert is missing or within 30 days of expiry, PINT requests a new pint_profile_signing certificate from FreeIPA and stores it. A background goroutine renews it daily as needed. Unlike the RadSec cert, a renewed profile signing cert takes effect on the next PINT restart (no FreeRADIUS reload is required).
The signature on a mobileconfig is only verified at installation time — existing installed profiles remain functional even if the signing cert later expires.
The FreeRADIUS image is built from dev/freeradius/Dockerfile. It contains the virtual server and module configuration that defines FreeRADIUS's behaviour; the runtime-variable parts (client lists, TLS config, certificate material) are injected by PINT via the Kubernetes Secrets described above.
Understanding what is baked into the image versus what PINT controls at runtime is key to debugging auth failures.
flowchart TB
subgraph Image ["Baked into image"]
R[radsec virtual server\n/etc/raddb/sites-enabled/radsec]
S[status virtual server\n/etc/raddb/sites-enabled/status]
E[eap module\n/etc/raddb/mods-enabled/eap]
end
subgraph Secrets ["Injected at runtime via K8s Secrets"]
CC[clients.conf\n/etc/pint/config/]
TLS[radsec-tls.conf\n/etc/pint/config/]
ST[status config + secret\n/etc/pint/config/]
RADSEC[tls.crt · tls.key · ca.pem\n/etc/pint/radsec/]
EAP[eap.crt · eap.key · wifi-ca.pem\n/etc/pint/eap/]
end
R -->|"$-INCLUDE"| CC
R -->|"$INCLUDE"| TLS
S -->|"$-INCLUDE"| ST
E --> RADSEC
E --> EAP
radsec virtual server listens on TCP port 2083. It $INCLUDEs radsec-tls.conf (the TLS block PINT generates, containing cert paths and CRL settings) and $-INCLUDEs clients.conf (the enrolled controller list). The $-INCLUDE variant is FreeRADIUS syntax for an optional include; the server starts even if the file is absent, which lets FreeRADIUS boot before PINT has written its first config.
status virtual server listens on UDP port 18121. It $-INCLUDEs the status client config written by PINT, which defines which CIDRs may query the status server and the shared secret required to do so.
eap module configures EAP-TLS as the only permitted EAP type. It references files from the pint-eap-server-cert Secret: eap.crt and eap.key (the server certificate presented to devices during the 802.1X handshake) and wifi-ca.pem (the WiFi CA chain used to verify client certs). This is a separate certificate from the RadSec TLS cert — the RadSec cert (from pint-radsec-server-certificates) is verified by routers during the outer mTLS handshake, while the EAP cert is verified by devices during inner 802.1X authentication. Both are Wireless CA-issued, but they serve distinct TLS contexts.
RadSec is RADIUS-over-TLS (RFC 6614). Instead of UDP with a shared secret, controllers open a persistent TCP connection on port 2083 and authenticate with a mutual-TLS handshake. Once the TLS session is established, standard RADIUS packets flow over it.
sequenceDiagram
participant C as WiFi Controller
participant FR as FreeRADIUS :2083
participant EAP as EAP-TLS Module
C->>FR: TCP connect
C->>FR: TLS handshake (mutual)
note over C,FR: Controller presents pint_radsec_client cert<br>FreeRADIUS presents pint_radsec_server cert<br>Both verified against respective CAs
C->>FR: RADIUS Access-Request (user EAP-TLS identity)
FR->>EAP: Begin EAP-TLS exchange
EAP-->>C: EAP-Request (TLS handshake fragments)
C-->>EAP: EAP-Response (user cert)
EAP->>EAP: Validate user cert against wifi-ca.pem
EAP-->>FR: Accept / Reject
FR-->>C: RADIUS Access-Accept / Access-Reject
Source IP allowlists are enforced in clients.conf before any authentication occurs. A controller arriving from an unexpected IP is silently dropped at the RADIUS layer.
EAP-TLS is the only supported authentication method with no password fallback. During the EAP exchange, FreeRADIUS validates the user's client certificate against wifi-ca.pem (the WiFi intermediate CA). Only certificates issued through PINT's pint_eap_client profile will pass, since that profile enforces clientAuth EKU and the CA is not publicly trusted.
TLS 1.2 is the minimum version. The cipher list is restricted to ECDHE+AESGCM:DHE+AESGCM with secp384r1 as the negotiated ECDH curve. These control the TLS session's key exchange and are independent of the client certificate's key type — WiFi client certs may be RSA 2048 (iOS via SCEP) or EC (other platforms), both work with these cipher suites.
FreeRADIUS exposes a status virtual server on UDP port 18121. PINT queries each pod's status server directly (by pod IP, not through the Service) to surface per-pod statistics on the /status page: authentication counters, reject counts, and uptime. The shared secret is stored in pint-config under status-secret. PINT generates it once on first startup; subsequent restarts reuse the existing value.
The chart in chart/ deploys both PINT and FreeRADIUS into a single namespace and wires up the Kubernetes RBAC, Secrets, ConfigMap, and Services they need.
Key values:
# Container images; default tags come from Chart.appVersion
pint:
image:
repository: pint
tag: ""
freeradius:
image:
repository: pint-freeradius
tag: ""
# Non-sensitive config rendered into a ConfigMap
config:
clientID: ""
serverURL: ""
ipaHost: ""
ipaServiceAccount: ""
wifiSSID: "CSH"
radiusServer: ""
# ... (see chart/values.yaml for full list)
# Pre-existing Secret with sensitive credentials (see below)
envSecret: ""
# OpenShift Route (disabled by default)
openshift:
enabled: false
route:
host: ""The FreeRADIUS Service defaults to LoadBalancer so port 2083 gets an external IP. In environments without a load balancer (like the dev kind cluster), set freeradius.service.type=NodePort and specify a nodePort.
To disable the in-cluster PINT deployment and run PINT locally instead (useful during development):
pint:
enabled: falseThe chart is published to GitHub Pages via helm/chart-releaser-action on every push to main or dev that touches chart/**.
main: releases the version declared inchart/Chart.yamlas a stable release.dev: stamps the version as<version>-dev.<run_number>(e.g.0.1.0-dev.42) and publishes a pre-release. Useful for testing chart changes before merging.
To add the Helm repository:
helm repo add pint https://computersciencehouse.github.io/pint
helm repo updateAll non-sensitive PINT configuration is rendered directly into the Deployment's env block from the config: values. Sensitive credentials must be provided in a pre-existing Secret:
kubectl create secret generic <release-name> -n pint \
--from-literal=PINT_CLIENT_SECRET=<oidc-secret> \
--from-literal=PINT_IPA_PASSWORD=<ipa-password> \
--from-literal=PINT_SESSION_SECRET=$(openssl rand -base64 32)Set envSecret in your values to the name of this Secret. The chart mounts it via envFrom on the PINT Deployment.
| Tool | Purpose |
|---|---|
| Go 1.26+ | Build PINT and the FreeIPA stub |
| Docker | Build images |
kind |
Local Kubernetes cluster for FreeRADIUS |
helm |
Deploy the chart into kind |
kubectl |
Interact with the dev cluster |
overmind |
Run the Procfile (PINT + FreeIPA stub simultaneously) |
caddy |
HTTPS reverse proxy for local SCEP testing (iOS requires TLS) |
# One-time: create the kind cluster, install the Helm chart,
# build the FreeRADIUS image, and install metrics-server.
# Safe to re-run; skips steps already complete.
make dev-setup
# Copy and edit the dev env file.
# The stub defaults work for all IPA_* fields out of the box.
cp .env.dev.example .env.dev
# Build both binaries and start everything.
make devmake dev starts three processes via overmind and the Procfile:
ipa-stub: FreeIPA stub server on:8088(see FreeIPA Stub below).pint: the PINT server on:8080. It waits for the stub to be ready before starting.caddy: HTTPS reverse proxy fromhttps://localhost:8443tohttp://localhost:8080. Only starts whenPINT_SERVER_URL=https://localhost:8443(see Testing SCEP Locally below); otherwise it idles.
FreeRADIUS runs in the kind cluster and persists between make dev sessions. PINT talks to it via the Kubernetes API using your local ~/.kube/config.
To access RTP-gated routes (/status reload button, /admin/radius) locally, set PINT_DEV_RTP=true in .env.dev.
Other useful targets:
make build # compile pint binary
make build-stub # compile freeipa-stub binary
make test # go test ./... -v
make lint # go vet ./...
make dev-logs # stream FreeRADIUS logs from the kind cluster
make dev-forward # port-forward RadSec to localhost:2083
make dev-metrics # (re-)install metrics-server (enables CPU/memory on /status)
make docker-build # build pint:dev Docker image
make clean # remove binaries, kill stub process
All configuration is via environment variables. Copy .env.dev.example to .env.dev to get started.
Required:
| Variable | Description |
|---|---|
PINT_CLIENT_ID |
Keycloak OIDC client ID |
PINT_CLIENT_SECRET |
Keycloak OIDC client secret |
PINT_SESSION_SECRET |
Secret key for cookie-backed sessions; generate with openssl rand -base64 32 |
PINT_SERVER_URL |
Public base URL (e.g. https://pint.csh.rit.edu) |
PINT_IPA_HOST |
FreeIPA hostname (e.g. ipa.csh.rit.edu) |
PINT_IPA_SERVICE_ACCOUNT |
FreeIPA service account DN (krbprincipalname=pint/host@REALM,...) |
PINT_IPA_PASSWORD |
FreeIPA service account password |
PINT_WIFI_SSID |
SSID embedded in generated WiFi profiles |
PINT_RADIUS_SERVER |
RadSec endpoint shown to users (e.g. radius.csh.rit.edu:2083) |
Optional (defaults shown):
| Variable | Default | Description |
|---|---|---|
PINT_IPA_WIRELESS_CA_NAME |
wireless |
FreeIPA CA for WiFi client certs |
PINT_IPA_RADSEC_CA_NAME |
radsec |
FreeIPA CA for RadSec certs |
PINT_IPA_ROOT_CA_NAME |
ipa |
Root signing CA |
PINT_IPA_EAP_CLIENT_CERT_PROFILE |
pint_eap_client |
Dogtag profile for EAP-TLS client certs |
PINT_IPA_RADSEC_CLIENT_CERT_PROFILE |
pint_radsec_client |
Dogtag profile for controller certs |
PINT_IPA_RADSEC_SERVER_CERT_PROFILE |
pint_radsec_server |
Dogtag profile for the RadSec server cert |
PINT_IPA_EAP_CERT_PROFILE |
pint_radsec_server |
Dogtag profile for the EAP-TLS server cert (Wireless CA-issued; presented to devices during 802.1X) |
PINT_IPA_CODE_SIGNING_CA_NAME |
(unset) | FreeIPA intermediate CA for profile signing certs; enables iOS mobileconfig signing when set |
PINT_IPA_CODE_SIGNING_CERT_PROFILE |
pint_profile_signing |
Dogtag profile for the profile signing cert |
PINT_PROFILE_SIGNING_CERT_SECRET |
pint-profile-signing-cert |
K8s Secret for the profile signing cert and key |
PINT_NAMESPACE |
pint |
Kubernetes namespace |
PINT_CONFIG_SECRET |
pint-config |
K8s Secret for RADIUS config |
PINT_RADSEC_CERT_SECRET |
pint-radsec-server-certificates |
K8s Secret for RadSec TLS material (tls.crt, tls.key, ca.pem) |
PINT_EAP_CERT_SECRET |
pint-eap-server-cert |
K8s Secret for EAP-TLS server cert material (eap.crt, eap.key, wifi-ca.pem) |
PINT_FREERADIUS_DEPLOYMENT |
pint-freeradius |
FreeRADIUS Deployment name |
PINT_RADIUS_STATUS_PORT |
18121 |
FreeRADIUS status server port |
PINT_RADIUS_RADSEC_CHECK_CRL |
true |
Enable CRL checking in the RadSec TLS listener |
PINT_RADIUS_RADSEC_PROXY_PROTOCOL |
false |
Expect HAProxy PROXY protocol header on RadSec connections; set true when HAProxy fronts FreeRADIUS |
PINT_SCEP_RA_CERT_SECRET |
pint-scep-ra-cert |
K8s Secret for the SCEP Registration Authority (RA) certificate and key; auto-generated on first startup |
PINT_IPA_SKIP_TLS_VERIFY |
false |
Skip FreeIPA TLS verification (dev only) |
PINT_DISABLE_OIDC |
false |
Bypass OIDC and inject a static dev user |
PINT_DEV_RTP |
false |
Inject rtp group into dev user (requires PINT_DISABLE_OIDC=true) |
iOS requires HTTPS to complete SCEP enrollment, so local testing needs a TLS frontend. Caddy handles this automatically when you set PINT_SERVER_URL to the local HTTPS address in .env.dev:
# .env.dev
PINT_SERVER_URL=https://localhost:8443
PINT_DISABLE_OIDC=true # skip OIDC so you can download profiles without a Keycloak sessionWith those two variables set, make dev will start Caddy alongside PINT. On first run, Caddy generates a local CA and certificate. Trust it system-wide so the iOS profile installer accepts the mobileconfig:
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain \
"$HOME/Library/Application Support/Caddy/pki/authorities/local/root.crt"After that, download a profile from https://localhost:8443/profile, install it on a device on the same network, and watch PINT's logs for the SCEP PKIOperation request and the resulting cert issuance. The SCEP Registration Authority (RA) certificate is auto-generated and stored in the pint-scep-ra-cert Kubernetes Secret on first startup; it persists across restarts.
The stub (dev/freeipa-stub/) is a minimal HTTPS server that implements just enough of the FreeIPA JSON-RPC API for PINT to function locally. It runs on :8088 with a self-signed TLS certificate, so PINT_IPA_SKIP_TLS_VERIFY=true must be set in .env.dev.
CA structure
On first run the stub generates a three-tier CA hierarchy and persists it to dev/freeipa-stub/data/:
Root CA (ipa)
├── WiFi CA (wireless) # signs pint_eap_client and pint_radsec_server certs
├── RadSec CA (radsec) # signs pint_radsec_client certs
└── Code Signing CA (code_signing) # signs pint_profile_signing certs (optional)
The CA names are read from PINT_IPA_WIRELESS_CA_NAME, PINT_IPA_RADSEC_CA_NAME, and PINT_IPA_ROOT_CA_NAME at startup and must match the values in .env.dev. On subsequent runs the persisted keys and certificates are reloaded, so issued certificates remain valid across restarts.
Profile signing is optional in local dev. To enable it, uncomment the three PINT_IPA_CODE_SIGNING_CA_NAME lines in .env.dev. On the next make dev run the stub will generate a code_signing intermediate CA under the root, persist it to dev/freeipa-stub/data/, and handle cert_request calls for the pint_profile_signing profile with codeSigning EKU. Leaving the variable unset skips signing entirely; PINT starts normally and generates unsigned profiles.
Implemented RPC methods
| Method | Behaviour |
|---|---|
ca_show |
Returns the DER-encoded certificate for the named CA |
cert_request |
Signs the CSR with the requested CA; applies profile-appropriate EKU and validity (see below) |
cert_revoke |
No-op; always returns success |
Authentication (/ipa/session/login_password) accepts any credentials and returns a stub session cookie.
Profile handling
The stub maps profile IDs to EKU and validity:
| Profile ID | EKU | Validity | Notes |
|---|---|---|---|
pint_radsec_server |
serverAuth |
90 days | DNS SAN set to CSR CN (required for Go TLS verification) |
pint_profile_signing |
codeSigning |
1 year | Only available when PINT_IPA_CODE_SIGNING_CA_NAME is set |
pint_eap_client |
clientAuth |
1 year | Accepts RSA and EC public keys |
pint_radsec_client and others |
clientAuth |
5 years |
Unlike real FreeIPA/Dogtag, the stub does not enforce subject name patterns or key type constraints defined in the profile config files.
An end-to-end integration test that exercises the full EAP-TLS authentication path over a live RadSec connection to the kind cluster.
# Requires make dev running with the FreeIPA stub on :8088
make radsec-smoketestWhat it does:
- Builds a smoketest Docker image (
debian:trixie-slim+eapol_test+freeradius) and loads it into kind. - Uses the running FreeIPA stub to issue a controller client cert and a user WiFi cert.
- Launches a Kubernetes pod that starts a local FreeRADIUS instance in proxy-only mode, configured to forward requests over RadSec (mTLS) to
pint-freeradius.pint.svc.cluster.local:2083. - Runs
eapol_testwith the user WiFi cert to perform a real EAP-TLS authentication end to end. - Exits 0 on success, 1 on failure. Cleans up the pod and cert Secret either way.
