Skip to content

Rate limiting, audit and metrics for the osctrld endpoints - #1097

Merged
javuto merged 1 commit into
developfrom
osctrld-rate-limits
Oct 18, 2026
Merged

javuto merged 1 commit into
developfrom
osctrld-rate-limits

Conversation

@javuto

@javuto javuto commented Oct 18, 2026

Copy link
Copy Markdown
Collaborator

Rate limiting, audit and metrics for the osctrld endpoints

Follow-up to extensions delivery (#1096).

Problem

osctrl-tls's osctrld routes (osctrld-flags, -cert, -verify, -script, -extensions, -extension) authenticate with the shared environment secret, and they had:

  • no rate limit;
  • no audit of failed secrets;
  • no metrics.

Anyone who knew an environment's UUID could guess secrets without limit or trace. The secret now unlocks flags, certificates and root-run extension binaries.

What changes

Three new limits under rateLimits, with the same fields as enroll (burst, period, evictAfter, retryAfter, maxBuckets), CLI flags (--rate-limit-osctrld-auth-*, --rate-limit-osctrld-*, --rate-limit-osctrld-download-*) and env vars. They are validated at startup.

Limit Default Keyed by Counts
osctrldAuth 10 per hour, Retry-After 360 s client address + environment wrong secrets only
osctrld 300 per minute client address every osctrld request
osctrldDownload 30 per minute client address extension downloads
  • Blocking: once an address used up its failed-secret budget for an environment, it gets 429 with Retry-After on that environment's osctrld routes, even with the right secret, until the budget refills.
    • Correct secrets never use the budget, so many hosts behind one NAT address aren't affected by one misconfigured or hostile neighbour.
    • Other environments and addresses are unaffected.
  • Enforcement:
    • A new middleware around all six routes checks the traffic, download and auth limits before the body is read.
    • The secret comparison then checks and charges the auth budget atomically, so slow parallel requests can't get extra guesses.
  • Client address: utils.GetIP is used for limiter keys and audit. It is the TCP peer, or the right-most untrusted forwarding hop when the peer is in --trusted-proxies.
    • Behind a proxy or load balancer, trustedProxies must be set; otherwise every client shares the proxy's budgets.
    • The enroll limiter is unchanged.
  • Many addresses: past maxBuckets tracked keys, new keys share one overflow budget and are blocked collectively.
  • Audit:
    • Every wrong secret is written to the audit log with address and environment (new auditlog.FailedOsctrld).
    • While an address is blocked, only the first rejection is written, then at most one per period.
    • A per-environment cap bounds the total audit writes.
  • Metrics:
    • osctrld_auth_failures_total{environment}, the alert signal for secret guessing;
    • osctrld_rate_limited_total{route,limit};
    • osctrld_auth_audit_dropped_total{environment};
    • per-route request latency in the existing histogram, labelled by route pattern, not the raw URL.
  • Shared secret check: a single helper now does the secret check for all six handlers.
  • CertHandler: its 403 body is now empty, previously "uh oh...".

Configuration compatibility

  • An existing tls.yml without the new entries still loads: the defaults are filled in before the YAML is read.
  • Database service config without them keeps the defaults.
  • ConfigVersion 4 → 5. This only affects the startup notice and the generated samples. Sample tls.yml/api.yml, the service-config help text and ARCHITECTURE.md are updated.

Security and operational impact

  • Secret guessing: limited to 10 wrong secrets per address and environment per hour, then blocked, audited and visible in metrics. Legitimate osctrld traffic never sends wrong secrets.
  • Proxies: deployments behind a reverse proxy or load balancer must set trustedProxies, or all hosts share one budget.
  • Replicas: budgets are per osctrl-tls process; N replicas give N times each budget.
  • Mixed fleets: older osctrld clients are unaffected unless they exceed the budgets, and they retry on their next sync.

Validation

  • go test ./... -count=1 passes (new tests also with -race)
  • go vet ./...; gofmt -l pkg cmd (empty); golangci-lint run ./...: 0 issues
  • tls/api/cli build; make openapi-check clean
  • New tests, each seen failing first:
    • Limiter primitives: the non-consuming check, atomic check-and-charge, overflow, Retry-After.
    • Shared NAT: 100 correct-secret requests from one address are never blocked.
    • Blocking: 10 failures block only that address and environment, even with the right secret afterwards; the block refills; upper-case UUIDs share a budget.
    • Concurrent wrong secrets are charged atomically.
    • Trusted proxies: clients behind one proxy get separate budgets, and a spoofed left-most X-Forwarded-For doesn't change the key.
    • Traffic and download limits.
    • Audit: throttled while blocked, global cap, dropped-audit counter.
    • Metrics: the counters, and latency labelled by route pattern.
    • All six handlers charge failed secrets.
    • Full route registration: every osctrld route returns 429 when blocked, and enroll is unaffected.
    • Configuration: defaults, validation and flags; old YAML loads; DB service config; the sample config documents every limit.
  • No frontend behaviour change beyond the service-config help text.

@javuto javuto added ✨ enhancement New feature or request osctrld osctrld related issues labels Oct 18, 2026
@javuto
javuto merged commit bc23d7f into develop Oct 18, 2026
8 checks passed
@javuto
javuto deleted the osctrld-rate-limits branch October 18, 2026 14:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ enhancement New feature or request osctrld osctrld related issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant