A lightweight authentication proxy built on PostgreSQL, PostgREST, and nginx. It accepts external IAM JWTs, persists normalized claims, and issues internally signed EdDSA JWTs fit for batch/Grid jobs. A subsequent RPC lets clients obtain a short‑lived IAM bearer when needed.
Core design: only EdDSA (Ed25519) is enabled by default. An HS256 path is available as an optional SQL file.
- Architecture
- Prerequisites
- Database Installation
- JWKS.json (internal + external issuers)
- PostgREST Configuration
- nginx Reverse Proxy
- Operational Flow
- Usage Examples
- Validation & Smoke Tests
- Optional: HS256 Legacy Path
- Security Notes
- Troubleshooting
Client (OIDC/IAM) ──(Bearer ext)──> /rpc/register_my_jwt (role: anon)
│
├─ verifies & persists claims in api.mbox
└─ issues maiproxy JWT (EdDSA, 4–7 days)
Batch job ──(maiproxy JWT)──> /rpc/get_iam_jwt (role: virgo|igwn)
└─ returns a valid external IAM JWT (bearer)
Core DB objects (schema api):
users,mbox,eventsregister_my_jwt(),get_iam_jwt(message jsonb),jwt_decode(text)- trigger
mbox_eddsa_bi→mbox_eddsa_insert()
Core crypto (schema sec):
sign_jwt_eddsa(...),add_jwt_claims(...), base64url helpersderive_eddsa_privkey/pubkey(...),export_eddsa_jwk(...)
- PostgreSQL 18+
- PostgREST 14+
pgsodiumextension (installed cluster‑wide)pgcrypto(standard)- nginx with a valid TLS certificate for the public hostname
- Access to at least one IAM issuer (e.g. INFN‑CNAF Virgo IAM)
Ensure the database user
authenticatorexists with a strong password.
Load SQL files in this exact order:
01_roles.sql– roles & memberships (no personal accounts)02_extensions.sql–pgsodium,pgcrypto03_core.sql– schemas, tables, EdDSA functions, trigger, grants- (Optional)
90_optional_hs256.sql– legacy HS256 path (disabled)
psql -U postgres -d maiproxy -f sql/01_roles.sql
psql -U postgres -d maiproxy -f sql/02_extensions.sql
psql -U postgres -d maiproxy -f sql/03_core.sql
# optional
psql -U postgres -d maiproxy -f sql/90_optional_hs256.sql- Internal:
pgsodium_admin(no login),sec_owner(no login),api_owner(no login) - External:
authenticator(login, NOINHERIT),anon(no login),virgo(no login),igwn(no login)
- No direct table grants to client roles
- RPC is executed via SECURITY DEFINER functions owned by
api_owner
PostgREST loads a JWKS file to verify inbound JWTs. Combine:
- External issuers (e.g. INFN IAM JWKS)
- maiproxy’s internal Ed25519 public key from DB (keypair id
2)
- Export maiproxy JWK from PostgreSQL:
SELECT sec.export_eddsa_jwk(2) AS jwk; -- use the same keypair_id used to sign- Create
/etc/postgrest/JWKS.json:
{
"keys": [
{ "kty": "OKP", "crv": "Ed25519", "x": "<base64url_pubkey>", "kid": "ed25519-2", "use": "sig", "alg": "EdDSA" },
{ "kty": "RSA", "e": "AQAB", "n": "<...>", "kid": "iam-virgo-key", "alg": "RS256", "use": "sig" }
]
}- Secure permissions:
chmod 600 /etc/postgrest/JWKS.json
chown postgrest /etc/postgrest/JWKS.jsonYou can automate external JWKS refresh with a cron job downloading the upstream JWKs and merging them with the local one.
Create prest_igwn.conf:
db-uri = "postgres://authenticator:<db_secure_password>@<database_host>:5432/maiproxy"
db-anon-role = "anon"
db-schemas = "api"
server-host = "127.0.0.1"
server-port = "3000"
jwt-secret = "@/etc/postgrest/JWKS.json"
log-level = "debug"Notes
db-anon-role = "anon"allows calling/rpc/register_my_jwteven if the inbound IAM token has noroleclaim.- maiproxy’s EdDSA JWT does include a
role(e.g.virgoorigwn), so subsequent calls (e.g./rpc/get_iam_jwt) will run under the mapped DB role after PostgREST verification.
Expose PostgREST securely over TLS. Example snippets:
# RPC endpoints: register_my_jwt, get_iam_jwt
location /rpc/ {
default_type application/json;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Authorization $http_authorization;
# Forwarding context (helpful for logs / auth)
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://postgrest;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
client_max_body_size 10m;
# Inspect upstream (debug)
add_header X-Upstream-Addr $upstream_addr always;
add_header X-Upstream-Status $upstream_status always;
}
# to access views/tables if needed
location /api/ {
default_type application/json;
proxy_hide_header Content-Location;
add_header Content-Location /api/$upstream_http_content_location;
proxy_set_header Connection "";
proxy_http_version 1.1;
proxy_pass http://postgrest/;
}Ensure an upstream
upstream postgrest { server 127.0.0.1:3000; }block exists and the server listens on:8443with a valid certificate.
- Client obtains fresh IAM token (external OIDC
oidc-token, VOMS, etc.) - Register it to maiproxy via
/rpc/register_my_jwt(executes asanonin DB) - maiproxy returns an EdDSA JWT (valid ~4–7 days) → store and distribute to batch jobs
- The batch uses maiproxy JWT to call
/rpc/get_iam_jwtwhen it needs a short‑lived IAM bearer
# 1) Fetch fresh IAM token (repeat periodically, e.g. every 30 minutes)
BEARER_TOKEN=$(oidc-token -f myclient)
MAIPROXY_EP="https://<webservice_host>:8443"
touch maiproxy.jwt && chmod 600 maiproxy.jwt
# 2) Register IAM token with maiproxy
curl --capath /etc/grid-security/certificates -fsS -X POST \
-H "Authorization: Bearer ${BEARER_TOKEN}" \
-H "Content-Type: application/json" \
${MAIPROXY_EP}/rpc/register_my_jwt > maiproxy.jwtMAIPROXY_EP="https://<webservice_host>:8443"
IGWN_JWT=$(cat maiproxy.jwt)
MSG='{"message":{"jobid":"123.4","runtime":321,"reason":"Hi there!"}}'
touch iam.jwt && chmod 600 iam.jwt
curl --capath /etc/grid-security/certificates -sS -X POST \
-H "Authorization: Bearer ${IGWN_JWT}" \
-H "Content-Type: application/json" \
--data "${MSG}" \
${MAIPROXY_EP}/rpc/get_iam_jwt > iam.jwt- Expired external IAM token →
/rpc/register_my_jwtmust fail with a 4xx error. - Old maiproxy JWT (>7 days) →
/rpc/get_iam_jwtshould fail (expired or no valid match). - Tampered
JWKS.json→ PostgREST should reject inbound JWTs (401/403). - Call
/rpc/get_iam_jwtwithanon→ forbidden (role must map tovirgoorigwn). - Event logging →
api.eventsshould record renew/reuse/served events for auditing.
The repository includes sql/90_optional_hs256.sql which:
- Adds HS256 signing/verify helpers in
sec.* - Adds legacy
api.mbox_insert()and a disabled triggermbox_before_insert
To enable HS256 (not recommended unless needed):
CREATE EXTENSION IF NOT EXISTS pgjwt; -- provide public.sign
ALTER TABLE api.mbox ENABLE TRIGGER mbox_before_insert;
ALTER TABLE api.mbox DISABLE TRIGGER mbox_eddsa_bi; -- switch off EdDSA path- Use a strong password for
authenticatorand do not grant it extra privileges. - Prefer no direct table access for external roles; use RPC with SECURITY DEFINER.
- Rotate Ed25519 keypair id if necessary; publish its JWK via
sec.export_eddsa_jwk(<id>).
- 401/403 from PostgREST: verify
jwt-secretpath, JWKS contents, and tokens’kid/alg. register_my_jwtfails for known users: ensure the subject exists inapi.usersand that headers includeAuthorization: Bearer ....get_iam_jwtreturns no match: check thatapi.mboxcontains a recent row with matchingsub/scope/wlcg.groupsand that the bearer is not near expiry (exp > now + 600s).- nginx shows 502: inspect
X-Upstream-Statusheaders and PostgREST logs; confirmpostgrestis reachable at127.0.0.1:3000.
EUPL 1.2