Deployment¶
How to run credgate for a team: the proxy that people's tools talk to, and the app (the policy editor, in R or Python) where InfoSec manages the policy and everyone manages their own key - on one server or several. Everything here is open source with permissive licences (credgate is Apache-2.0; nginx is BSD, OAuth2 Proxy MIT, Keycloak and Caddy Apache-2.0).
people's tools ──(their own cgk_ key)──► credgate proxy ──(endpoint keys)──► Anthropic, OpenAI,
(Claude Code, SDKs, notebooks) (one or more) vLLM, Ollama ...
│ reads (and reloads)
policy.yaml (+ tests, detectors) · keys.json · users.yaml
│ writes
people's browsers ──► web server with single sign-on ──► credgate app (127.0.0.1 only)
(nginx/Apache/Caddy + OAuth2 Proxy or Keycloak)
The files¶
Keep them together, owned by a service account, readable by the proxy and writable by the app:
| File | What | Changed by |
|---|---|---|
policy.yaml, policy.tests.yaml, detector files |
the policy and its tests | admins, in the app |
policy.changes.jsonl |
change log of the policy (hash-chained) | the app, on each save |
keys.json |
each person's key hash and expiry | everyone (own key) and admins, in the app; credgate proxy add-user ... |
keys.access.jsonl |
who made, renewed or removed keys (hash-chained) | the app |
users.yaml |
who may use the app, their role, whether active | the super admin, on the server (credgate users ...), or an export from AD/LDAP |
audit.jsonl (monthly files) |
every request through the proxy, without content | the proxy |
/srv/credgate/
policy.yaml policy.tests.yaml detectors/ policy.changes.jsonl
keys.json keys.access.jsonl users.yaml
audit/ # one audit log per proxy instance
The proxy¶
credgate proxy serve \
--policy /srv/credgate/policy.yaml --keys /srv/credgate/keys.json \
--users /srv/credgate/users.yaml --require-tests \
--audit /srv/credgate/audit/audit-$(hostname).jsonl \
--host 0.0.0.0 --port 8790 --certfile proxy.pem --keyfile proxy.key \
--server uvicorn --workers 4 --watch 10
- The endpoints' own keys go in the service's environment
(
CREDGATE_UPSTREAM_KEY_CLAUDE=...), never in files people can read. --watch 10picks up a new policy revision, a changed keys file or users file within 10 seconds; a revision that does not load or fails its tests is refused and the running one stays (details).- Run it as a systemd service, with
ExecReload=/bin/kill -HUP $MAINPIDsosystemctl reloadreloads at once. /healthz(no key needed) shows the running revision, for monitoring and load balancers.
The app, behind single sign-on¶
The app does not handle passwords. A web server in front signs people in
with the bank's single sign-on and passes the signed-in name to the app in a
header; the app looks the name up in users.yaml for the role.
1. Run the app on 127.0.0.1 only, so it is reachable through the sign-in proxy alone (otherwise anyone could send the header themselves):
# app.R, run with: Rscript -e 'shiny::runApp("app.R", host = "127.0.0.1", port = 3838)'
credgate::policy_editor(
"/srv/credgate/policy.yaml",
users = "/srv/credgate/users.yaml",
keys = "/srv/credgate/keys.json",
audit = "/srv/credgate/audit/audit-proxy1.jsonl",
trust_header = "X-Auth-Request-User"
)
Run it with shiny::runApp() as above (as a systemd service) rather than
Shiny Server: the open-source Shiny Server is AGPL-licensed.
Or the same app in Python (pip install 'credgate[editor]'; Shiny for
Python is MIT-licensed and runs on uvicorn):
credgate editor --policy /srv/credgate/policy.yaml --users /srv/credgate/users.yaml \
--keys /srv/credgate/keys.json --audit /srv/credgate/audit/audit-proxy1.jsonl \
--trust-header X-Auth-Request-User --host 127.0.0.1 --port 3838 --no-browser
It refuses --trust-header unless it listens on 127.0.0.1.
2. Put the sign-in proxy in front. With nginx and OAuth2 Proxy (connected to the bank's identity provider - Azure AD/Entra ID, Okta, Keycloak, ADFS):
server {
listen 443 ssl;
server_name credgate.bank.internal;
# ssl_certificate ...;
location /oauth2/ { proxy_pass http://127.0.0.1:4180; }
location / {
auth_request /oauth2/auth;
error_page 401 = /oauth2/sign_in;
auth_request_set $user $upstream_http_x_auth_request_user;
proxy_set_header X-Auth-Request-User $user; # replaces anything the browser sent
proxy_pass http://127.0.0.1:3838;
proxy_http_version 1.1; # Shiny uses WebSockets
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
OAuth2 Proxy is started with --set-xauthrequest so it returns the user
(and their groups) in X-Auth-Request-* headers. Keycloak, or Apache with
mod_auth_openidc, work the same way: whatever signs people in passes the
name in a header that the app trusts (trust_header).
3. Make the name match. The name in the header must be the name in
users.yaml and keys.json - often the e-mail address or the directory
account name (priya@bank.example or priya).
Roles and leavers from the directory (AD, LDAP)¶
users.yaml can be written by hand (credgate users add ...), or generated
from directory groups by a scheduled job, so joiners and leavers follow the
directory. For example, nightly from an AD group (using OpenLDAP's
ldapsearch):
#!/bin/sh
# users.yaml from AD groups: credgate-admins, credgate-auditors, credgate-users.
set -e
f=/srv/credgate/users.yaml.new
for role in admin auditor user; do
ldapsearch -LLL -H ldaps://ad.bank.internal -b "dc=bank,dc=internal" \
"(memberOf=CN=credgate-${role}s,OU=Groups,DC=bank,DC=internal)" sAMAccountName |
awk '/^sAMAccountName:/ {print $2}' |
while read -r name; do credgate users add "$name" --role "$role" --file "$f"; done
done
mv "$f" /srv/credgate/users.yaml # replaced at once; the proxy and app reload it
Someone removed from the groups disappears from users.yaml at the next
run: the app shows them "No access", and the proxy refuses their key
("no longer has access") even before it expires. To cut access at once,
credgate users deactivate <name> --file /srv/credgate/users.yaml.
The super admin is whoever can run these commands on the server: there is no super admin account in the app.
Daily housekeeping¶
# keys not used for 60 days expire (judged from the audit log)
credgate proxy expire-unused --keys /srv/credgate/keys.json \
--audit /srv/credgate/audit/audit-proxy1.jsonl --days 60
Audit logs rotate monthly and are pruned after the retention period
(--audit-retention-years, default 5). Back up /srv/credgate, keeping the
change and access logs: they are the evidence of who changed what.
Several servers¶
Run the proxy on several servers behind a load balancer (health check
GET /healthz, which answers 503 while draining). Every instance needs the
same policy, keys and users files:
- Shared storage (NFS, a cluster file system) mounted at
/srv/credgateon every server, with--watch: the app writes each file whole and renames it into place, so a proxy never reads half a file. Keep one audit log per instance (audit-$(hostname).jsonl): file locking is not reliable on network file systems. - Pushed files (Ansible, Puppet, a Kubernetes ConfigMap and Secret):
the app runs on one server and writes there; deployment copies the files
to every instance and reloads them (
systemctl reload, or--watch).
Either way, /healthz on each instance shows its policy revision, so you can
check that every instance has the new one. Give the app and the keys file
one home - the server the app runs on, or the shared storage - so that keys
are made in one place.
Checklist¶
- [ ] The app listens on
127.0.0.1and is reachable only through the sign-in proxy;trust_headernames the header that proxy sets. - [ ] TLS on the sign-in proxy and the credgate proxy (or the load balancer).
- [ ] Endpoint keys only in the proxy service's environment.
- [ ]
users.yamlhas at least one active admin; the super admin can reach the server's command line. - [ ] The proxy runs with
--users,--watchand--require-tests. - [ ] Daily
expire-unused; backups of/srv/credgate. - [ ] Network rules make the proxy the only way to the external model endpoints.