Let an operator authenticate with a seeded key, and reset the schema
- TERDUT_OPERATOR_KEY creates or re-keys the instance-scoped service account
"terdut-operator" at every start, so terdut-operator needs no bootstrap
handshake. An instance-scoped account now acts as owner of every team's
configuration, but is not a member of any team.
- POST /api/teams takes an external_id (instance service accounts only) and
is idempotent on it, so automation finds its own team again after a crash
instead of adopting by display name. GET /api/teams?name= is removed.
- Integration and dead man's switch names are unique per team (409). The
escalation PUT accepts usernames and resolves them itself.
- The 18 migrations are squashed into 001_schema.sql, with no Default team.
TERDUT_DEADMAN_* and the env seeding of switches are removed: teams carry
their own. Existing development databases must be recreated.
Security and robustness:
- GET /api/users no longer returns other people's email or ntfy topic to
non-admins.
- The access log records the route pattern, so integration keys and ack
tokens in the path are not written to the log. Server errors are logged.
- Rate limits take the client address TERDUT_TRUSTED_PROXIES hops from the
right of X-Forwarded-For instead of trusting the first, forgeable entry.
- /api/bootstrap runs in a transaction under an advisory lock, so two
concurrent calls cannot both create an administrator.
- API key last_used_at is written at most every five minutes.
Cleanup: remove GET /api/incidents/{id}/alerts, unused exports, SQLite
remnants in comments and config.
Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
This commit is contained in:
@@ -95,12 +95,6 @@ spec:
|
||||
value: "{{ .Values.sweeper.staleAfter }}"
|
||||
- name: TERDUT_ARCHIVE_AFTER
|
||||
value: "{{ .Values.sweeper.archiveAfter }}"
|
||||
- name: TERDUT_DEADMAN_MATCHERS
|
||||
value: "{{ .Values.deadman.matchers }}"
|
||||
- name: TERDUT_DEADMAN_TIMEOUT
|
||||
value: "{{ .Values.deadman.timeout }}"
|
||||
- name: TERDUT_DEADMAN_SEVERITY
|
||||
value: "{{ .Values.deadman.severity }}"
|
||||
{{- if .Values.notify.ntfyUrl }}
|
||||
- name: TERDUT_NTFY_URL
|
||||
value: "{{ .Values.notify.ntfyUrl }}"
|
||||
@@ -122,6 +116,8 @@ spec:
|
||||
value: "{{ .Values.notify.publicUrl | default (printf "https://%s" .Values.networking.hostname) }}"
|
||||
- name: TERDUT_PASSWORD_LOGIN
|
||||
value: {{ .Values.passwordLogin | quote }}
|
||||
- name: TERDUT_TRUSTED_PROXIES
|
||||
value: {{ .Values.trustedProxies | quote }}
|
||||
- name: TERDUT_OPERATOR_MODE
|
||||
value: {{ .Values.operatorMode | quote }}
|
||||
{{- if .Values.oidc.enabled }}
|
||||
|
||||
@@ -72,43 +72,10 @@ sweeper:
|
||||
# How long a resolved alert stays in the default list before auto-archiving.
|
||||
archiveAfter: 168h
|
||||
|
||||
# Alerts treated as dead man's switches: receiving one opens no incident, and
|
||||
# the absence of one does. The Watchdog alert kube-prometheus-stack ships is
|
||||
# exactly this — an always-firing alert whose only value is something noticing
|
||||
# when it stops.
|
||||
deadman:
|
||||
# Which alerts to treat as heartbeats. ";" separates matchers, "," separates
|
||||
# the label conditions within one, "=" is exact equality. Every matcher must
|
||||
# name an alertname:
|
||||
# alertname=Watchdog,cluster=prod; alertname=EdgeHeartbeat
|
||||
# Each distinct label set is watched independently, so two clusters sending
|
||||
# the same alertname are two switches and a live one cannot mask a dead one.
|
||||
matchers: "alertname=Watchdog"
|
||||
# How long a heartbeat may go unheard before its switch is declared dead.
|
||||
#
|
||||
# This must be SHORTER than the Alertmanager repeat_interval of the route
|
||||
# carrying the heartbeat — the opposite of sweeper.staleAfter. The default
|
||||
# repeat_interval of 4h (12h in many setups) makes for a useless dead man's
|
||||
# switch, so give the heartbeat a route of its own:
|
||||
#
|
||||
# - matchers: [ 'alertname = "Watchdog"' ]
|
||||
# receiver: terdut
|
||||
# group_wait: 0s
|
||||
# group_interval: 1m
|
||||
# repeat_interval: 1m
|
||||
#
|
||||
# That delivers every 2m rather than every 1m: a group is only reconsidered
|
||||
# each group_interval, and at exactly one elapsed interval repeat_interval has
|
||||
# not quite passed, so equal values give 2x. Fine against 15m; use
|
||||
# group_interval: 30s if you want a true 1m.
|
||||
#
|
||||
# Set to 0 to disable dead man's switch handling entirely.
|
||||
timeout: 15m
|
||||
# Severity a dead man's switch incident opens at. These incidents have no
|
||||
# member alerts to derive one from, and the heartbeat's own severity label is
|
||||
# meaningless — Watchdog ships as "none". Only "critical" maps to the ntfy
|
||||
# priority that overrides a phone's quiet hours.
|
||||
severity: critical
|
||||
# How many reverse proxies in front of the server append to X-Forwarded-For.
|
||||
# The per-address login/sign-up rate limits take the client address that many
|
||||
# entries from the right. 0 ignores the header.
|
||||
trustedProxies: 1
|
||||
|
||||
notify:
|
||||
# ntfy server that push notifications are published to, e.g.
|
||||
@@ -190,10 +157,8 @@ oidc:
|
||||
# Hard ceiling on a session made by a single sign-on login.
|
||||
sessionMaxAge: 12h
|
||||
|
||||
# Backups are no longer this chart's business. The SQLite database lived on a PVC
|
||||
# beside the app, so it needed a sidecar with a sqlite3 module for k8up to exec a
|
||||
# dump in; Postgres is backed up where it runs, through a k8up.io/backupcommand
|
||||
# pg_dump annotation on the database pod itself.
|
||||
# Backups are not this chart's business: Postgres is backed up where it runs,
|
||||
# through a k8up.io/backupcommand pg_dump annotation on the database pod itself.
|
||||
|
||||
bootstrap:
|
||||
enabled: true
|
||||
|
||||
Reference in New Issue
Block a user