Probes and metrics
| Endpoint | Authentication | What |
|---|---|---|
GET /healthz | none | The server and its database: {"status": "ok", "database": "ok"}. Liveness |
GET /readyz | none | Readiness, with the database pinged (2 s deadline) and email status |
GET /readyz/selfguard | none | Fails (503 selfguard_draining) only while selfguard drains the node; does not check the database. The Helm chart's readiness probe |
GET /metrics | NI_AUTH_METRICS_TOKEN or a super admin | Prometheus metrics for every organisation |
GET /metrics/selfguard | the same | Break-glass status that works while the database is down |
GET /metrics/selfguard/rollout-gate | the same | The rollout gate, for deploy tooling |
NI_AUTH_METRICS_TOKEN must be at least 32 bytes (openssl rand -base64 48). Scrape with it as a bearer token.
The probes, /metrics and /metrics/selfguard are exempt from admission control, so they answer while the node is overloaded.
Selfguard
Selfguard keeps each Govern process healthy on its own. It watches the process and the dependencies Govern needs (PostgreSQL first), detects anomalies, sheds load under pressure, repairs what it safely can, verifies that the repair worked, and escalates to operators when it did not. It is about Govern itself, not tenant workloads.
- Air-gap first. No external calls. Alerts go to the log and, optionally, to a webhook you run.
- Audited. Every automatic or operator action and every finding transition is written to the audit ledger in the same transaction as the change.
- Database outages are the main case. Detection, healing and alerting run from memory; history is written to PostgreSQL in the background, buffered (up to 10,000 records) while it is down.
- Safe by construction. Actions run under a mode, a cluster-wide kill switch, cooldowns, an hourly budget (12 per node), prechecks and post-action verification. Disruptive actions need an operator's approval.
It also runs in the data plane (keystone-bridge), the compliance plane (keystone-comply), the provisioner and the sandbox daemons, from memory and without history or approvals.
What it watches
| Area | Signals |
|---|---|
| HTTP | Requests, errors, error ratio, p50 and p99 latency, shed requests |
| Go runtime | Goroutines, heap, memory against GOMEMLIMIT or the container limit, GC pauses, scheduler latency |
| Process | Open file descriptors against the limit |
| Database pool | In use, idle, queueing time |
| PostgreSQL server | Connection slots in use, longest transaction, sessions idle in a transaction, lock waits, replication lag (one node samples every 30 s) |
| Canaries | The server fetches its own JWKS and OpenID discovery document over loopback every 30 s |
| Disks | Free space of the diagnostics, temporary and data directories, and NI_AUTH_SELFGUARD_DISK_PATHS |
| Certificates | Expiry of the configured certificate files and NI_AUTH_SELFGUARD_CERT_FILES, re-read hourly |
| Background jobs | Heartbeats, runs, durations, failures, crash loops and stalls of every supervised job |
| Cluster | The same finding on most nodes, lost nodes, silent nodes, and a new release that regresses against the previous one |
Set GOMEMLIMIT or a container memory limit; without either, memory pressure cannot be measured.
Detectors and playbooks
| Rule | Condition | Playbook |
|---|---|---|
runtime.memory_pressure | Memory at 85% of the limit for 3 ticks | capture diagnostics, release memory, shed batch, drain node |
process.fd_exhaustion | 85% of file descriptors for 2 ticks | capture diagnostics, recycle DB connections, shed batch, drain node |
db.pool_saturation | Pool 90% in use and queueing | shed batch, capture diagnostics |
db.server_connections | 90% of PostgreSQL connection slots, 2 samples | recycle DB connections, capture diagnostics |
db.idle_in_transaction | A Govern session idle in a transaction for 10 min | terminate idle transactions (approval) |
probe.failing/database | The database probe failed 3 times | recycle DB connections |
http.slo_fast_burn | Availability budget burning 14.4 times too fast over 1 h and 5 min | capture diagnostics |
http.latency_anomaly | p99 well above the learned baseline for that hour of the week | shed batch, capture diagnostics |
job.stalled | A job missed its heartbeat or overran | restart the job |
disk.pressure | Below 15% and 10 GiB free (warning), or below 1 GiB (critical) | prune diagnostics |
cert.expiry | A certificate expires within 30 days (critical within 7) | operators |
The full list, with thresholds, is in the operator runbook shipped with each release. Baselines warm up first and never fire before that.
Admission control
The outermost HTTP handler limits concurrency adaptively (starting at 256, between 32 and 4096) and classifies requests:
| Class | Requests | Admitted while in flight is below |
|---|---|---|
| exempt | /healthz, /livez, /readyz, /readyz/selfguard, /metrics, /metrics/selfguard | always |
| critical | /.well-known/, token, revoke, userinfo and introspection endpoints, /api/v1/auth/, /api/v1/authz/check, the selfguard API | 125% of the limit |
| normal | everything else | 100% (90% in protect mode) |
| batch | paths with a segment such as export, exports, download, report, bulk, import, reindex | 70% (0% in protect mode) |
Once in-flight requests reach half the limit, one organisation may hold at most a quarter of it (tenant bulkheads). A rejected request gets 503 (node overloaded) or 429 (tenant share exceeded) with Retry-After and X-Keystone-Shed. Long-lived streams give their slot back once they start. NI_AUTH_SELFGUARD_ADMISSION=measure measures without rejecting.
Operate selfguard
All routes are for super admins. Bodies are JSON; every change needs a reason or note, recorded in the audit event.
export KS=https://<your Govern host>
alias ks='curl -sS -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json"'
ks $KS/api/v1/admin/selfguard/nodes
ks "$KS/api/v1/admin/selfguard/findings?state=open"
ks "$KS/api/v1/admin/selfguard/actions?limit=20"
If the API answers 401 during an incident, the database is probably down and sessions cannot be checked. Use the break-glass status with the metrics token:
curl -sS -H "Authorization: Bearer $NI_AUTH_METRICS_TOKEN" $KS/metrics/selfguard
| Task | Request |
|---|---|
| Acknowledge a finding | POST /api/v1/admin/selfguard/findings/{id}/acknowledge {"note": "..."} |
| Approve or reject a disruptive action | POST /api/v1/admin/selfguard/actions/{id}/approve or /reject {"note": "..."}, within the approval window (15 min) |
| Pause every automatic action (kill switch) | PATCH /api/v1/admin/selfguard/settings {"actions_paused": true, "paused_reason": "..."} |
| See what it would do without acting | PATCH .../settings {"mode": "dry_run", "reason": "..."}; back with "enforce" |
| Run an action on a node now | POST /api/v1/admin/selfguard/actions {"node_id", "action", "subject"?, "note"} |
| Rehearse detection and alerting | POST /api/v1/admin/selfguard/drills {"node_id", "rule", "severity", "duration", "live"?, "note"} |
The kill switch reaches every node within one tick (10 s). Detection and alerting continue while actions are paused. A drill raises a finding whose message starts with [DRILL]; its playbook runs as a dry run unless "live": true. Run a drill after enabling selfguard, after changing the alert webhook, and at on-call handover.
Disruptive actions:
drain_nodemakes/readyzanswer503for 3 minutes, so load balancers route around the node. It is never proposed for the only active node or while another node drains. Approve when the node is clearly sick and the others have headroom.terminate_idle_transactionsends Govern's own sessions idle in a transaction for over 10 minutes. Before approving, look at what they hold (pg_stat_activity); reject when it is a migration or a maintenance session. Then setidle_in_transaction_session_timeouton Govern's database role.
Diagnostic bundles (goroutines.txt, heap.pprof, status.json) are written under NI_AUTH_SELFGUARD_DIAGNOSTICS_DIR; mount a volume there to keep them across restarts.
Alerts
Alerts go to the structured log and, when NI_AUTH_SELFGUARD_ALERT_WEBHOOK_URL is set, are posted as JSON with kinds finding.opened, finding.escalated, finding.resolved, action.approval_required, action.failed, action.unverified and remediation.exhausted. Each request carries X-Keystone-Timestamp and X-Keystone-Signature: sha256=<hex HMAC-SHA256(secret, timestamp + "." + body)>; reject timestamps older than a few minutes. Delivery is retried up to 5 times.
The rollout gate
curl --fail -sS -H "Authorization: Bearer $NI_AUTH_METRICS_TOKEN" \
"$KS/metrics/selfguard/rollout-gate?release=$TAG&min_soak=10m&strict=true"
It answers {"release", "proceed", "reasons", ...} and, with strict=true, 412 while: no node reports the release; a node running it is not ready or has an open critical finding; it has run for less than min_soak; it regresses against the previous release (error ratio more than twice, or p99 more than 1.5 times, the baseline's, with at least 200 requests on each side); or a critical cluster-wide finding is open. Use it as an Argo Rollouts analysis or a Helm post-upgrade hook. Govern does not roll back by itself; the gate lets your tooling do it. The Helm chart sets NI_AUTH_SELFGUARD_RELEASE to the image tag.
Settings
All optional. An invalid value stops the start with a message naming it.
| Variable | Default | Meaning |
|---|---|---|
NI_AUTH_SELFGUARD_ENABLED | true | false turns detection, healing and shedding off; jobs stay supervised |
NI_AUTH_SELFGUARD_MODE | enforce | observe, dry_run or enforce, until an operator stores a mode |
NI_AUTH_SELFGUARD_TICK | 10s | Detection interval |
NI_AUTH_SELFGUARD_MAX_ACTIONS_PER_HOUR | 12 | Automatic actions per node per hour |
NI_AUTH_SELFGUARD_AUTO_APPROVE | empty | Disruptive actions that run without approval, for example drain_node |
NI_AUTH_SELFGUARD_APPROVAL_TTL | 15m | Approval window |
NI_AUTH_SELFGUARD_DIAGNOSTICS_DIR | $TMPDIR/keystone-selfguard | Diagnostic bundles |
NI_AUTH_SELFGUARD_DIAGNOSTICS_KEEP | 10 | Bundles kept |
NI_AUTH_SELFGUARD_DRAIN_DURATION | 3m | How long a drain reports not ready |
NI_AUTH_SELFGUARD_RETENTION_DAYS | 90 | History retention |
NI_AUTH_SELFGUARD_SLO_AVAILABILITY | 0.999 | Availability objective of the burn-rate rules |
NI_AUTH_SELFGUARD_MEMORY_PRESSURE_RATIO | 0.85 | Memory pressure threshold |
NI_AUTH_SELFGUARD_ALERT_WEBHOOK_URL, NI_AUTH_SELFGUARD_ALERT_WEBHOOK_SECRET | empty | Alert webhook and its HMAC secret (at least 32 bytes) |
NI_AUTH_SELFGUARD_ADMISSION | on | on or measure |
NI_AUTH_SELFGUARD_ADMISSION_INITIAL_LIMIT, _MIN_LIMIT, _MAX_LIMIT | 256, 32, 4096 | Adaptive limit bounds |
NI_AUTH_SELFGUARD_ADMISSION_STATIC_LIMIT | 0 | Above 0 pins the limit |
NI_AUTH_SELFGUARD_ADMISSION_TENANT_SHARE | 0.25 | One organisation's share once bulkheads engage |
NI_AUTH_SELFGUARD_RELEASE | build version | The release the rollout gate compares |
NI_AUTH_SELFGUARD_DISK_PATHS | empty | Extra filesystems to watch, label=/path |
NI_AUTH_SELFGUARD_CERT_FILES | empty | Certificate files to watch, label=/file.pem |
Give server pods a terminationGracePeriodSeconds of at least 30 (the chart uses 60), the bridge at least 50 and the compliance plane at least 45, so their last flushes finish on shutdown.
Metrics to alert on
| Metric | Alert when |
|---|---|
keystone_selfguard_findings_active{rule,severity} | Any critical finding is active |
keystone_selfguard_draining | A node drains |
keystone_selfguard_journal_dropped_total | It increases: history was dropped during a long database outage |
keystone_selfguard_shed_requests_total{priority,reason} | Normal or critical traffic is shed |
keystone_selfguard_breaker_state{breaker} | A breaker is open (2) |
keystone_email_delivery_healthy | It is 0 for 10 minutes |
Not covered yet
- No console page for selfguard beyond the platform area's self-healing page; the API is complete.
- No automatic rollback of a bad release: the rollout gate's
412lets your tooling do it. - No circuit breaker per model provider in the model gateway yet.