SAML, LDAP, OIDC/SSO, Proxies, TLS, and Network Security: Guided Hands-On Workflow
Build a disposable local HTTPS reverse-proxy lab, validate forwarded headers and server base URL, exercise fake SAML/LDAP/OIDC configuration evidence, and keep a tested local rollback path.
Learning objectives
- Create a local HTTPS reverse proxy in front of a disposable SonarQube Community Build instance without disabling TLS verification.
- Prove the external/base URL and forwarded-header contract.
- Preserve one deliberately broken forwarded-protocol configuration and repair it causally.
- Validate fake SAML metadata/callback/group mapping without a production IdP.
- Model LDAP and OIDC configuration boundaries safely without storing real provider credentials.
- Disconnect/rollback cleanly while preserving a local administrator path.
1. Disposable lab contract
http://127.0.0.1:9000. The
HTTPS proxy listens only on loopback. The certificate is self-signed
specifically for localhost and is trusted explicitly
with --cacert; never replace that with
-k/--insecure.
| State | Lab value |
|---|---|
| SonarQube | Community Build 26.9.0.129388 |
| Backend URL | http://127.0.0.1:9000 |
| External lab URL | https://localhost:9443 |
| Proxy | Small Python 3.11+ local-only reverse proxy for teaching; not a production proxy |
| IdP | Fake local metadata/claims only |
| Secrets | No real IdP secret, LDAP password, private enterprise certificate, or scanner token |
2. Preflight: record current server and recovery state
mkdir -p sq-ch22-network-lab/evidence
cd sq-ch22-network-lab
python --version
openssl version
curl -fsS http://127.0.0.1:9000/api/system/status \
| tee evidence/backend-system-status.json
In SonarQube, record the current Server base URL and verify a local administrator login works at the backend URL. Do not disable or delete that account during the lab.
X-Forwarded-Proto: http to https changes
forwarded transport evidence without changing SonarQube, source
code, users, or permissions.
3. Create a localhost certificate with a Subject Alternative Name
cat > openssl-local.cnf <<'EOF'
[req]
distinguished_name = dn
x509_extensions = v3
prompt = no
[dn]
CN = localhost
[v3]
subjectAltName = @alt
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
basicConstraints = CA:FALSE
[alt]
DNS.1 = localhost
IP.1 = 127.0.0.1
EOF
openssl req -x509 -newkey rsa:3072 -sha256 -nodes \
-days 2 -keyout localhost.key -out localhost.crt \
-config openssl-local.cnf
openssl x509 -in localhost.crt -noout -subject -issuer -dates -ext subjectAltName \
| tee evidence/certificate.txt
chmod 600 localhost.key 2>/dev/null || true
The private key remains local and is deleted during cleanup. The evidence packet contains only non-secret certificate metadata, not the private key.
4. Build a tiny teaching proxy with an intentional header fault
This proxy is deliberately minimal so the learner can see exactly where trust is asserted. It is not a substitute for Nginx, IIS, HAProxy, Caddy, an ingress controller, or a managed load balancer.
# proxy.py — local lab only
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from http.client import HTTPConnection
import ssl, sys
BACKEND = ("127.0.0.1", 9000)
FORWARDED_PROTO = sys.argv[1] if len(sys.argv) > 1 else "http" # broken first
class Proxy(BaseHTTPRequestHandler):
def _forward(self):
body = self.rfile.read(int(self.headers.get("Content-Length", "0")))
headers = {k: v for k, v in self.headers.items()
if k.lower() not in {"host", "connection", "x-forwarded-proto", "x-forwarded-for"}}
headers["Host"] = "localhost:9443"
headers["X-Forwarded-Proto"] = FORWARDED_PROTO
headers["X-Forwarded-For"] = self.client_address[0]
print(f"FORWARD proto={FORWARDED_PROTO} client={self.client_address[0]} path={self.path}", flush=True)
conn = HTTPConnection(*BACKEND, timeout=20)
conn.request(self.command, self.path, body=body, headers=headers)
resp = conn.getresponse()
data = resp.read()
self.send_response(resp.status)
for k, v in resp.getheaders():
if k.lower() not in {"transfer-encoding", "connection"}:
self.send_header(k, v)
self.end_headers()
self.wfile.write(data)
conn.close()
do_GET = _forward
do_POST = _forward
def log_message(self, fmt, *args):
print("CLIENT " + (fmt % args), flush=True)
server = ThreadingHTTPServer(("127.0.0.1", 9443), Proxy)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain("localhost.crt", "localhost.key")
server.socket = ctx.wrap_socket(server.socket, server_side=True)
server.serve_forever()
# Broken first run: proxy forwards proto=http even though the client uses HTTPS.
python proxy.py http > evidence/proxy-broken.log 2>&1 &
echo $! > proxy.pid
sleep 1
curl --cacert localhost.crt -fsS \
https://localhost:9443/api/system/status \
| tee evidence/proxy-broken-status.json
grep 'FORWARD proto=' evidence/proxy-broken.log
The request may still return HTTP 200 because the backend API itself is reachable; the preserved log proves the proxy is lying about the external transport. That is a configuration defect even before a SAML redirect exposes it.
5. Repair only the forwarded protocol
kill "$(cat proxy.pid)"
python proxy.py https > evidence/proxy-fixed.log 2>&1 &
echo $! > proxy.pid
sleep 1
curl --cacert localhost.crt -fsS \
https://localhost:9443/api/system/status \
| tee evidence/proxy-fixed-status.json
grep 'FORWARD proto=' evidence/proxy-fixed.log
Backend SonarQube version, user permissions, database, source code, and project state did not change. Only the proxy’s transport assertion changed. That isolation is the core diagnostic skill.
6. Validate Server base URL without losing rollback
For this disposable lab only, set Server base URL to
https://localhost:9443 under
Administration → Configuration → General Settings →
General. Record the old value first.
before_base_url: <record exact value>
lab_base_url: https://localhost:9443
rollback: restore before_base_url after SSO/proxy validation
Do not set a production base URL to localhost. The goal is to prove that external origin, proxy Host/proto, and SAML callback calculations must agree.
7. Build fake SAML configuration evidence — no external IdP call
<!-- fake-idp-metadata.xml: synthetic evidence only -->
<EntityDescriptor entityID="https://idp.example.invalid/saml">
<IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="https://idp.example.invalid/sso" />
</IDPSSODescriptor>
</EntityDescriptor>
{
"sp_entity_id": "sq-ch22-local",
"server_base_url": "https://localhost:9443",
"reply_url": "https://localhost:9443/oauth2/callback/saml",
"login_attribute": "uid",
"name_attribute": "displayName",
"email_attribute": "mail",
"group_attribute": "groups"
}
Never insert this fake IdP URL into a production instance. The artifact exists to validate the mapping contract and callback math safely.
8. Validate group claims offline before touching authorization
{
"uid": "alice-lab",
"displayName": "Alice Lab",
"mail": "alice@example.invalid",
"groups": ["sq-ch22-developers"]
}
# validate_claims.py
import json
c = json.load(open("fake-claims.json"))
required = {"uid", "displayName", "groups"}
missing = required - c.keys()
assert not missing, f"missing attributes: {sorted(missing)}"
allowed_groups = {"sq-ch22-developers"}
unexpected = set(c["groups"]) - allowed_groups
assert not unexpected, f"unexpected privileged groups: {sorted(unexpected)}"
print("mapping contract OK")
Create the nonprivileged sq-ch22-developers group
locally if you want to validate how a synchronized membership would
map to existing SonarQube permissions. Do not map the synthetic
claim to sonar-administrators.
9. LDAP path: configuration simulation with an explicit trust note
# ldap-lab.properties — do not apply unless you have a local disposable LDAP server
sonar.security.realm=LDAP
ldap.url=ldaps://ldap.example.invalid:636
ldap.bindDn=cn=sq-reader,ou=svc,dc=example,dc=invalid
ldap.bindPassword=<runtime-secret>
ldap.user.baseDn=ou=people,dc=example,dc=invalid
ldap.group.baseDn=ou=groups,dc=example,dc=invalid
Record: if this were a real LDAPS endpoint, its CA/server certificate would need to be trusted by the SonarQube Java runtime. The repair for a trust failure is to fix the trust chain/hostname—not to disable certificate verification.
10. OIDC path: faithful configuration simulation, not a native-feature claim
{
"issuer": "https://oidc.example.invalid",
"authorization_endpoint": "https://oidc.example.invalid/authorize",
"token_endpoint": "https://oidc.example.invalid/token",
"jwks_uri": "https://oidc.example.invalid/jwks",
"redirect_uri_example": "https://localhost:9443/oauth2/callback/oidc",
"note": "third-party plugin/broker simulation only; recheck plugin compatibility before use"
}
A community OIDC plugin exists, but plugin installation changes server compatibility/security state and requires restart/maintenance governance. It is not part of the mandatory lab. Prefer built-in SAML where it meets the IdP requirement.
11. Disconnect and restore
- Restore the original Server base URL.
-
Stop the local proxy with
kill "$(cat proxy.pid)". -
Delete
localhost.keyafter preserving only non-secret certificate metadata. - Remove only synthetic groups/users created for this lab.
- Verify the local administrator still authenticates directly to the backend/local instance.
- Keep proxy logs, base-URL before/after record, certificate metadata, and fake IdP mapping files as evidence.
12. Challenge: which layer owns this failure?
A browser trusts the TLS certificate and reaches the SonarQube login page, but SAML returns “invalid recipient/ACS URL.” The scanner still analyzes successfully with its project token. What should you inspect?
Answer path: preserve browser/proxy/Sonarqube web
logs, compare Server base URL with the IdP Reply URL, verify Host
and X-Forwarded-Proto=https, and confirm the SAML
callback path. Do not rotate the scanner token: scanner
authentication is a separate trust path.
Knowledge check
Why does the lab use
--cacert localhost.crt instead of
-k?
It verifies the intended certificate explicitly. Disabling TLS verification would hide the very trust boundary the lab is meant to test.
The HTTPS API returns 200 while the proxy log says
proto=http. Is the proxy correct?
No. The transport assertion is wrong even if this endpoint still works. SAML/redirect behavior can fail later.
Why validate group claims offline first?
It catches mapping/privilege mistakes before a real IdP login can grant unintended SonarQube group membership.
What should happen to the local recovery administrator during the lab?
It remains available and is tested before and after the external-authentication simulation.
What is the safe response to an LDAPS hostname/trust failure?
Correct the CA chain, certificate identity, DNS/hostname, and Java trust configuration. Do not disable verification.
Official references and version notes
- Community Build — authentication and provisioning overview — current native delegated-authentication list and JIT/group-synchronization model.
- Community Build — LDAP — LDAP/AD authentication, group synchronization, LDAPS/trust guidance, logs and migration notes.
- Community Build — SAML overview — SP-initiated SAML, IdP certificate, attributes and group synchronization.
- Community Build — Server base URL — canonical external URL required for authentication/integration correctness.
- Community Build — securing behind a proxy — inbound HTTP model, TLS termination, required forwarded headers, and proxy examples.
- Community Build — networking requirements — external systems, base-URL reachability, HTTPS/proxy topology, and network rules.
-
Community Build — SAML with Microsoft Entra ID
— Reply URL format
/oauth2/callback/samland base-URL dependency. - Community OIDC plugin (third party) — optional generic OIDC path; explicitly not a first-party SonarSource native authentication guarantee.
Rechecked 2026-09-08. Mandatory examples target
Community Build 26.9.0.129388. Current Community
Build documentation lists HTTP-header, LDAP, SAML, GitHub,
Bitbucket Cloud, and GitLab authentication; LDAP and SAML are
available in Community Build with JIT/group synchronization.
Generic OIDC is not listed as a native method in the current
Community Build authentication overview, so this chapter treats it
as a configuration simulation or optional third-party-plugin path
requiring independent compatibility/security review. SonarQube
accepts inbound application traffic as HTTP; production HTTPS is
terminated at a reverse proxy/ingress/load balancer. For
HTTPS/SAML, X-Forwarded-Proto and
X-Forwarded-For must be set by the trusted proxy.
Server base URL must match the externally reachable
origin/callback design. Automatic provisioning/SCIM and other
enterprise identity lifecycle features are
edition/provider-specific and are not required by the mandatory
local path. Recheck authentication support, plugin compatibility,
proxy headers, callback URLs, Java trust-store requirements, and
provisioning ownership before applying this material to another
release.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.