Lighthouse version: 0.20.x
Admin API base URL (via SSH tunnel):http://localhost:7673/api/v1/admin
Interactive docs (Swagger UI):http://localhost:7673/api/v1/admin/docs
The Admin API is the primary management interface for Lighthouse 0.20.x — it
replaces the old lhcli CLI tool for day-to-day operations (users, metadata,
lifetimes, key management, removal).
Enrollment note: the Admin API does not fetch a subordinate's keys automatically — see §5. To enroll a live entity by auto-fetching its keys, the
/enrollendpoint (§9) remains the simplest path.
Port 7673 is bound to the VM's loopback interface only and is additionally blocked by Caddy on port 443 as defense-in-depth. The only way to reach it is through an SSH tunnel.
Open the tunnel (keep this terminal open for the entire session):
ssh -L 7673:localhost:7673 -i ~/.ssh/trust_anchor_ed25519 \
user.name@trust-anchor.dep.dev.rciam.grnet.grWith an ~/.ssh/config entry this shortens to:
Host trust-anchor
HostName trust-anchor.dep.dev.rciam.grnet.gr
User user.name
IdentityFile ~/.ssh/trust_anchor_key
ssh -L 7673:localhost:7673 trust-anchorVerify the tunnel is working:
curl -s -o /dev/null -w '%{http_code}' http://localhost:7673/api/v1/admin/docs
# Expected: 200With the tunnel open, browse to:
http://localhost:7673/api/v1/admin/docs
Click Authorize in the top-right corner, enter your username and password, and explore or test any endpoint interactively.
No authentication is required to create the very first user. After the first user is created, every subsequent API call requires HTTP Basic Auth.
curl -s -X POST http://localhost:7673/api/v1/admin/users \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"YOUR_STRONG_PASSWORD"}'
# Expected: 201 CreatedStore the password in a secrets manager (Bitwarden, 1Password, etc.) — the Admin API password is never stored in the repository or Ansible Vault.
All calls require Basic Auth after the first user is created.
CREDS="admin:YOUR_ADMIN_PASSWORD"
# List users
curl -s -u "$CREDS" http://localhost:7673/api/v1/admin/users | python3 -m json.tool
# Create another admin user
curl -s -u "$CREDS" -X POST http://localhost:7673/api/v1/admin/users \
-H 'Content-Type: application/json' \
-d '{"username":"admin2","password":"another-strong-password"}'
# Change a user's password
curl -s -u "$CREDS" -X PUT http://localhost:7673/api/v1/admin/users/admin \
-H 'Content-Type: application/json' \
-d '{"password":"new-strong-password"}'
# Delete a user
curl -s -u "$CREDS" -X DELETE http://localhost:7673/api/v1/admin/users/admin2curl -s -u "$CREDS" \
http://localhost:7673/api/v1/admin/subordinates | python3 -m json.toolImportant: the Admin API does not fetch the subordinate's keys for you. The request field is
entity_id, and the defaultstatusisactive, which requires ajwksin the body. If you POST only anentity_idyou will get{"error":"invalid_request","error_description":"status cannot be active without keys"}. To enroll by automatically fetching the entity's keys from its/.well-known/openid-federation, use the/enrollendpoint instead (§9) — that is the only path that auto-fetches, and the recommended way to enroll a live entity.
To enroll through the Admin API you must supply the JWKS inline:
curl -s -u "$CREDS" \
-X POST http://localhost:7673/api/v1/admin/subordinates \
-H 'Content-Type: application/json' \
-d '{
"entity_id": "https://some-idp.example.org",
"jwks": { "keys": [ { "kty": "RSA", "kid": "...", "n": "...", "e": "AQAB", "use": "sig" } ] }
}'
# Expected: 201 Createdstatus accepts active, blocked, pending, or inactive (default
active).
Common error responses:
| HTTP | Meaning |
|---|---|
400 |
Invalid request — e.g. status cannot be active without keys (you set/defaulted status to active but supplied no jwks), or an invalid status value |
409 |
Already enrolled — POST is not idempotent. To update a record, use the jwks / status endpoints or PUT /subordinates/{id} (see Re-enroll below) |
# URL-encode the entity identifier
curl -s -u "$CREDS" \
"http://localhost:7673/api/v1/admin/subordinates/https%3A%2F%2Fsome-idp.example.org" \
| python3 -m json.toolPOST is not idempotent — re-posting an existing entity_id returns
409 Conflict. To update a subordinate's keys after they rotate, replace the
stored JWKS directly (URL-encode the entity_id in the path):
curl -s -u "$CREDS" \
-X PUT "http://localhost:7673/api/v1/admin/subordinates/https%3A%2F%2Fsome-idp.example.org/jwks" \
-H 'Content-Type: application/json' \
-d '{"keys":[ { "kty":"RSA", "kid":"...", "n":"...", "e":"AQAB", "use":"sig" } ]}'Alternatively, to re-fetch keys automatically from the entity, remove the
subordinate (below) and re-add it via /enroll (§9).
# URL-encode the entity identifier in the path
curl -s -u "$CREDS" -X DELETE \
"http://localhost:7673/api/v1/admin/subordinates/https%3A%2F%2Fsome-idp.example.org"
# Expected: 204 No ContentThe Entity Configuration is the signed JWT served at
/.well-known/openid-federation. Its metadata, lifetime, and signing
parameters are managed through the Admin API.
curl -s -u "$CREDS" \
http://localhost:7673/api/v1/admin/entity-configuration | python3 -m json.toolcurl -s -u "$CREDS" \
-X PUT http://localhost:7673/api/v1/admin/entity-configuration/metadata/federation_entity \
-H 'Content-Type: application/json' \
-d '{
"organization_name": "GRNET – Greek Research and Technology Network",
"display_name": "RI Federation Trust Anchor",
"contacts": ["aai-support@grnet.gr"],
"homepage_uri": "https://aai.grnet.gr"
}'How long the signed Entity Configuration JWT is valid before clients must re-fetch it:
curl -s -u "$CREDS" \
-X PUT http://localhost:7673/api/v1/admin/entity-configuration/lifetime \
-H 'Content-Type: application/json' \
-d '"1d"'How long the Trust Anchor's signed statements about subordinates are valid:
curl -s -u "$CREDS" \
-X PUT http://localhost:7673/api/v1/admin/statement-lifetime \
-H 'Content-Type: application/json' \
-d '"7d"'These endpoints are served by Caddy on port 443 and are publicly accessible:
BASE="https://trust-anchor.dep.dev.rciam.grnet.gr"
# Signed Entity Configuration JWT
curl -s "${BASE}/.well-known/openid-federation" | head -c 200
# List enrolled subordinate entity identifiers
curl -s "${BASE}/list"
# Fetch a signed statement for a specific subordinate
curl -s "${BASE}/fetch?sub=https://some-idp.example.org"
# Resolve full trust chain end-to-end
curl -s "${BASE}/resolve?sub=https://some-idp.example.org&trust_anchor=${BASE}"The /resolve call is the definitive end-to-end test — 200 OK with a JWT
means the full chain from the subordinate to the Trust Anchor is valid.
curl -s https://trust-anchor.dep.dev.rciam.grnet.gr/.well-known/openid-federation \
| python3 -c "
import sys, json, base64
t = sys.stdin.read().strip()
p = t.split('.')[1]; p += '=' * (-len(p) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(p)), indent=2))
"Expected top-level fields: iss, sub (both = entity_id), jwks,
metadata.federation_entity.* with all endpoint URLs.
Recommended for enrolling a live entity. Unlike the Admin API,
/enrollfetches and verifies the subordinate's keys for you. Use the Admin API (§5) for everything else — removal, metadata, lifetimes, key updates.
/enroll is a simple HTTP GET endpoint that fetches the subordinate's Entity
Configuration from its /.well-known/openid-federation, verifies it, extracts
the JWKS, and enrolls it in a single call. It is blocked by Caddy on port 443
(HTTP 403) and is reachable only via an SSH tunnel to the main server port
7672.
# Open a tunnel to the main server port (separate from the Admin API tunnel)
ssh -L 7672:localhost:7672 trust-anchor
# Enroll in a second terminal
curl -i "http://localhost:7672/enroll?sub=https://some-idp.example.org"
# Expected: 201 CreatedWhen to use /enroll vs the Admin API:
| Admin API (7673) | /enroll (7672) |
|
|---|---|---|
| Authentication | HTTP Basic Auth | None (admin access = open tunnel) |
| Enroll | POST /api/v1/admin/subordinates (you must supply jwks) |
GET /enroll?sub=... (auto-fetches keys) |
| Auto-fetches keys | No | Yes |
| Remove | DELETE /api/v1/admin/subordinates/{id} |
Not available |
| Metadata / key mgmt | Full CRUD | Not available |
| Recommended for | Removal, metadata, lifetimes, key updates | Enrolling a live entity |