0.28.0 — operational hardening (plan D1–D5): CI deploy gate, versioned images, rate limiting, resource limits, health probes
D1: deploy.sh CI gate — read-only SHA via git ls-remote, Gitea commit-status
poll, portable mkdir deploy lock (macOS, no flock), TOCTOU guard, token
passed via curl --config - (not argv), graceful misconfig tolerance.
D2: version-tagged images — OIKOS_VERSION=v$VERSION, keep-last-3 prune derived
from 'docker compose config --images'; VERSION read after pull.
D3: per-IP rate limiting — new internal/httpapi/ratelimit.go (x/time/rate),
rightmost-XFF, /healthz exempt, ctx-driven sweep; disabled by default.
D4: mem_limit/cpus on all 10 compose services.
D5: staleness-aware health probes — new internal/health package wired into
scheduler (:8093) and notifier (:8094); nomos already had :8092.
Two /review passes hardened the deploy lock, TOCTOU guard, token hygiene,
and XFF handling.
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
"github.com/dtoro/oikos/internal/config"
|
||||
"github.com/dtoro/oikos/internal/db"
|
||||
"github.com/dtoro/oikos/internal/health"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -25,7 +26,14 @@ import (
|
||||
func Run(ctx context.Context, pool *db.Pool, cfg config.Config) {
|
||||
slog.Info("notifier: starting")
|
||||
|
||||
// Liveness probe (plan D5): the notifier ticks every 15s (approvals) and
|
||||
// 30s (reactions). 2 min staleness covers a slow Matrix round-trip plus a
|
||||
// missed tick without false-failing.
|
||||
probe := health.New(2 * time.Minute)
|
||||
probe.Serve(ctx, cfg.HealthListen)
|
||||
|
||||
processPendingApprovals(ctx, pool, cfg)
|
||||
probe.Bump()
|
||||
|
||||
ticker := time.NewTicker(15 * time.Second)
|
||||
defer ticker.Stop()
|
||||
@@ -40,8 +48,10 @@ func Run(ctx context.Context, pool *db.Pool, cfg config.Config) {
|
||||
return
|
||||
case <-ticker.C:
|
||||
processPendingApprovals(ctx, pool, cfg)
|
||||
probe.Bump()
|
||||
case <-reactionTimer.C:
|
||||
pollReactions(ctx, pool, cfg)
|
||||
probe.Bump()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,13 +62,13 @@ func RunnerForMain() func(context.Context, *db.Pool, config.Config) {
|
||||
}
|
||||
|
||||
type pendingApproval struct {
|
||||
ID uuid.UUID
|
||||
Action string
|
||||
RiskClass string
|
||||
TokenHash *string
|
||||
AlertSentAt *time.Time
|
||||
MatrixEventID *string
|
||||
ExpiresAt time.Time
|
||||
ID uuid.UUID
|
||||
Action string
|
||||
RiskClass string
|
||||
TokenHash *string
|
||||
AlertSentAt *time.Time
|
||||
MatrixEventID *string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// processPendingApprovals finds pending approvals, generates tokens, and sends Matrix alerts.
|
||||
@@ -173,7 +183,7 @@ func checkReaction(ctx context.Context, cfg config.Config, roomID, eventID strin
|
||||
|
||||
var result struct {
|
||||
Chunk []struct {
|
||||
Type string `json:"type"`
|
||||
Type string `json:"type"`
|
||||
Content struct {
|
||||
RelatesTo map[string]string `json:"m.relates_to"`
|
||||
} `json:"content"`
|
||||
@@ -259,7 +269,9 @@ func sendMatrixAlert(ctx context.Context, cfg config.Config, approvalID uuid.UUI
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var mxResp struct{ EventID string `json:"event_id"` }
|
||||
var mxResp struct {
|
||||
EventID string `json:"event_id"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&mxResp)
|
||||
|
||||
if mxResp.EventID == "" {
|
||||
|
||||
Reference in New Issue
Block a user