GitHub
10/18/2026, 2:23 PMosctrld-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.
jmpsec/osctrlGitHub
10/18/2026, 2:42 PM