How to Configure Split-Horizon Networking¶
This guide covers configuring luci-sso when your router and your browser reach the identity provider at different network addresses — a common setup in home labs and self-hosted environments.
When you need this¶
Split-horizon applies when:
- Your IdP runs behind a reverse proxy or has a public hostname (e.g.,
auth.homelab.local) that the router cannot resolve, but your browser can. - Your router must reach the IdP via an internal IP or a different port (e.g.,
192.168.2.10:8443). - Your IdP's DNS name is only resolvable from your LAN, but your router uses a different DNS server or sits on a separate network segment.
sequenceDiagram
participant B as Browser
participant I as Identity Provider
participant R as Router (luci-sso)
note over B,I: Front-channel<br/>browser → auth.homelab.local
B->>I: 1. Redirect to login
I->>B: 2. Authorization code (via redirect)
B->>R: 3. Callback with code
note over R,I: Back-channel<br/>router → 192.168.2.10:8443
R->>I: 4. Token exchange
I->>R: 5. Tokens
R->>B: 6. Session cookie
The browser uses the public issuer_url for steps 1–3. The router uses internal_issuer_url for the back-channel token exchange (step 4), which never involves the browser.
What gets replaced¶
When internal_issuer_url is set, luci-sso replaces the origin (scheme + host) of the following back-channel URLs with the internal address:
| URL | Replaced? |
|---|---|
| Token endpoint | ✅ Yes |
| JWKS endpoint | ✅ Yes |
| UserInfo endpoint | ✅ Yes |
| Discovery document fetch | ✅ Yes |
| Authorization endpoint (browser redirect) | ❌ No — the browser handles this |
iss claim validation |
❌ No — always checked against issuer_url |
Only the origin is swapped; the path is preserved. https://auth.homelab.local/oauth/token becomes https://192.168.2.10:8443/oauth/token.
The IdP's discovery document must still declare the public issuer_url as its iss. luci-sso validates the issuer claim against the public address regardless of the internal URL.
Prerequisites¶
The internal address must:
- Use HTTPS — plain HTTP is rejected even for internal addresses.
- Have a certificate the router trusts. If the IdP uses a self-signed or private CA certificate, install it on the router:
# Copy your CA certificate to the router
scp -O ca.crt root@192.168.1.1:/etc/ssl/certs/my-homelab-ca.crt
# Update the CA bundle
update-ca-certificates
If the router cannot verify the IdP's certificate, the token exchange will fail with SSL_INIT_FAILED or a TOKEN_ENDPOINT_NETWORK_ERROR. See How to Debug luci-sso for log-based diagnosis.
Configuration¶
Set internal_issuer_url alongside the standard configuration:
Navigate to Services > SSO Login.
Fill in the Settings section with your standard provider credentials, then set Internal Issuer URL to the address the router uses to reach the IdP:
| Field | Value |
|---|---|
| Enable SSO | On |
| Issuer URL | https://auth.homelab.local |
| Client ID | luci-router |
| Client Secret | Your secret |
| Internal Issuer URL | https://192.168.2.10:8443 |
Click Save & Apply.
# Standard configuration
uci set luci-sso.default.issuer_url='https://auth.homelab.local'
uci set luci-sso.default.client_id='luci-router'
uci set luci-sso.default.client_secret='YOUR_SECRET_HERE'
uci set luci-sso.default.enabled='1'
# Split-horizon: the router reaches the IdP via this internal address
uci set luci-sso.default.internal_issuer_url='https://192.168.2.10:8443'
uci commit luci-sso
Verify¶
After committing, confirm the back-channel is working:
curl -sk https://localhost/cgi-bin/luci-sso?action=enabled
# Expected: {"enabled":true}
Then attempt a login from your browser. If the browser redirects to the IdP correctly but the router fails to exchange the code, the problem is in the back-channel. Check the log:
Navigate to Status > System Log and filter for luci-sso.
logread -e luci-sso | tail -30
Back-channel failures typically appear as TOKEN_EXCHANGE_FAILED, OIDC_DISCOVERY_FAILED, or JWKS_FETCH_FAILED. All three indicate the router cannot reach the internal address. Check:
- The router can reach
internal_issuer_url— test withcurl -sk <internal_issuer_url>/.well-known/openid-configurationfrom the router. - The certificate is trusted — test with
curl -s(without-k) to verify without skipping certificate checks. - The internal URL's origin exactly matches what
replace_originexpects — it must start with the same scheme and host asissuer_url, just with the origin swapped.
For the full list of error codes, see the Log Messages Reference.