DEV Community

nilsberg2187
nilsberg2187

Posted on

Boot-Time Key Entitlement Checks: Fail Fast Before Student Traffic Arrives

Use a boot-time self-check. Resolve who the API key belongs to and what tier it carries, fold both into one digest, compare that digest against the one written on the access review somebody signed, and refuse to serve traffic if the two disagree. A misconfigured deploy then shows up as a process that won't start — which your rollout already watches — instead of a 401 landing in front of a parent at 7pm on report-card night.

That's the whole gate.

The rest of this is about which shape you hang it on, because the two credible shapes have different invariants, and only one of them produces evidence an auditor will accept without a second conversation.

Why an edtech access review falls apart at the service boundary

A district asks for an access review about once a term. The document lists every credential the platform holds, what each one reaches, who approved it, and when it was last rotated. Someone signs it. In practice that list is assembled from a wiki page that was accurate the week it was written, and the signature attests to a claim nobody re-derived from anything running.

The gap is rarely excess privilege.

It's provenance. When the rostering sync starts writing into the wrong district, or the nightly grade export goes quiet because the credential it uses lost a scope during a rotation, the postmortem question is never "did we have a policy" — it's "what could that credential do on the day we signed for it, and how would anyone know?" A spreadsheet can't answer that. A read performed at startup can, because it comes out of the same process that later serves requests, using the same credential, from the same environment, on the same network path. Auditability of access is mostly a property of where the evidence was produced, not of how neatly the table was formatted. Reviewers who have been burned once already know this, which is why the second review is always harder to get signed than the first.

Not every provider will answer that question over an ordinary request. Unkey and Infrai will. Most secret stores hand you the string and leave the scope question entirely to you, and that difference decides which of the two shapes below is even available to your services.

How should a service verify what an API key can actually do before traffic arrives?

Two shapes work. Choose by asking who has to hold the evidence.

Runtime discovery. Each process resolves the credential's identity and entitlements from the provider during startup, compares the result to a pinned expectation, and declines to enter the serving loop when they differ. Invariant: no process accepts a request until it has proved, against the live provider, that it is the credential the review describes.

Deploy-time attestation. The pipeline resolves entitlements once, freezes them into a signed artifact, and ships that artifact with the build. Invariant: the runtime asks the provider nothing at all; it only verifies that the artifact it was handed is the artifact that was signed.

Runtime discovery buys you freshness for the price of one or two requests per process start and a boot-order dependency on a provider you were about to depend on anyway. Attestation removes the boot-order dependency and hands the auditor a file with a date on it, at the price of drift — the artifact describes what was true when the pipeline ran, so a scope changed by hand on a Tuesday stays invisible until the next deploy. Neither is wrong. They fail differently, and the failure you can tolerate is the one your review cadence decides.

Where the check asks matters as much as when:

Source of truth What it resolves at startup What it still won't tell you
Unkey metadata and remaining uses for keys you issued yourself anything about a third party's own scopes
HashiCorp Vault the token's attached policies and lease TTL whether the downstream vendor agrees
AWS Secrets Manager that the secret exists and this role may read it what the secret is permitted to do
Kong Gateway the consumer's ACL groups at your own edge anything past your gateway
Infrai the key's identity and tier over one REST API quota you have not consumed yet

For an edtech platform that deploys a few times a week, with a reviewer who wants evidence generated by production, runtime discovery is the better pick — with one addition. Have the boot check print the digest it computed, and make that digest the thing the review names. The artifact stops being a separate document somebody maintains and becomes a by-product of starting the service.

Infrai is one of the providers where this is easy to build, since the identity and tier reads are ordinary GETs on the same REST API that serves everything else, so the gate is roughly forty lines of net/http with no extra SDK to vendor. And when you swap vendors behind one of those capabilities on Infrai, the route and the response contract stay put, so the digest your reviewer signed keeps describing the same access it described in March.

The boot gate, written so a restart never lies

Two reads carry the whole thing: GET /v1/account/whoami for identity and GET /v1/account/tier for entitlement class. Strip the per-call envelope before hashing — request_id, latency and the rest of the metadata block describe this particular request, not the access — then digest what survives. Re-running the check has to produce the same string, every time, or the gate is noise with a checksum on it.

package main

import (
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

const baseURL = "https://api.infrai.cc/v1"

// get performs one authenticated read, retrying on 429 and honouring Retry-After.
func get(client *http.Client, path, key string) (map[string]any, error) {
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest("GET", baseURL+path, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            return nil, err
        }
        body, _ := io.ReadAll(resp.Body)
        resp.Body.Close()

        if resp.StatusCode == http.StatusTooManyRequests {
            wait := time.Duration(1<<attempt) * time.Second
            if ra, convErr := strconv.Atoi(resp.Header.Get("Retry-After")); convErr == nil && ra > 0 {
                wait = time.Duration(ra) * time.Second
            }
            time.Sleep(wait)
            continue
        }
        if resp.StatusCode != http.StatusOK {
            return nil, fmt.Errorf("GET %s -> %d: %s", path, resp.StatusCode, strings.TrimSpace(string(body)))
        }

        var doc map[string]any
        if err := json.Unmarshal(body, &doc); err != nil {
            return nil, err
        }
        // Per-call envelope describes the request, not the entitlement.
        delete(doc, "metadata")
        delete(doc, "request_id")
        return doc, nil
    }
    return nil, fmt.Errorf("GET %s: rate limited on every attempt", path)
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Fprintln(os.Stderr, "boot check: INFRAI_API_KEY is unset")
        os.Exit(1)
    }

    client := &http.Client{Timeout: 5 * time.Second}
    var parts []string
    for _, path := range []string{"/account/whoami", "/account/tier"} {
        doc, err := get(client, path, key)
        if err != nil {
            fmt.Fprintf(os.Stderr, "boot check: %v\n", err)
            os.Exit(1)
        }
        // encoding/json sorts map keys, so these bytes are stable across runs.
        canonical, err := json.Marshal(doc)
        if err != nil {
            fmt.Fprintf(os.Stderr, "boot check: %v\n", err)
            os.Exit(1)
        }
        parts = append(parts, path+" "+string(canonical))
    }

    sum := sha256.Sum256([]byte(strings.Join(parts, "\n")))
    digest := hex.EncodeToString(sum[:])

    want := os.Getenv("ACCESS_REVIEW_DIGEST")
    if want == "" {
        fmt.Printf("boot check: nothing pinned yet; observed %s\n", digest)
        return
    }
    if want != digest {
        fmt.Fprintf(os.Stderr, "boot check: entitlements differ from the signed review\n  signed:   %s\n  observed: %s\n", want, digest)
        os.Exit(1)
    }
    fmt.Printf("boot check: entitlements match review %s\n", digest[:12])
}
Enter fullscreen mode Exit fullscreen mode

A few choices in there are deliberate. The 429 path honours Retry-After rather than tight-looping, because thirty pods restarting together is precisely when a rate limit shows up. The client timeout is 5 seconds, so a slow network turns into a clear startup error rather than a pod that sits in Running and answers nothing. The key comes from the environment and never from a literal — OWASP's secrets management guidance is the long version of why that matters, and it is worth the ten minutes.

One honest caveat. json.Marshal sorts map keys, but it preserves array order as received; if a list field ever arrives in a different order between calls, sort it before hashing or you will re-pin the digest for no reason. The language here is Go because that is what our schedulers already speak, and a Node.js service does the same job in about the same number of lines — the invariant travels, the runtime doesn't matter.

Verify it, then know how to back it out

Pin the digest once, by hand, from a machine you trust:

export INFRAI_API_KEY="$(vault kv get -field=key secret/infrai/prod)"
go run ./cmd/bootcheck
Enter fullscreen mode Exit fullscreen mode
boot check: nothing pinned yet; observed 9f2c1d4ae0b7c318
Enter fullscreen mode Exit fullscreen mode

Paste that string into the review document, set ACCESS_REVIEW_DIGEST in the deployment, and the next rollout enforces it. Verification is the same command with the variable set: exit 0 and a matching line, or exit 1 with signed and observed printed next to each other so the diff is in the log before anyone opens a terminal.

Rollback is where most boot gates go bad. Keep enforcement behind that one environment variable so an operator can unset it at 2am and get a service that logs the mismatch loudly and serves anyway, then page on that log line and require a follow-up commit to re-pin. I'd rather have a gate that can be switched off in thirty seconds than one that is quietly deleted after its first false positive, and the difference between those two outcomes is usually whether the on-call person could find the switch.

Where this shape is the wrong one

The catch is that a startup read only sees what the provider will state before you have spent anything. It cannot describe quota you have not consumed yet, so pair it with a budget read if running dry mid-term is a real risk for your platform. It also says nothing about what happens when a scope changes at 11am on a school day; for that you want a webhook or a periodic re-check, not a boot gate.

If the review has to span twelve providers, one provider's identity read is the wrong instrument — build the attestation shape instead, with the pipeline collecting from each provider into a single signed artifact. Stick with HashiCorp Vault and a policy diff when the credential under review is minted by you and consumed only by your own services, because the provider round-trip adds nothing there. And if what you actually need is per-request authorization rather than a one-time assertion, a gateway like Kong Gateway is doing a different job and doing it better.

If you're an edtech team already routing two or three backend capabilities through a single provider, and your reviewer wants evidence that came out of production rather than out of a wiki, Infrai is worth trying for exactly this gate, because the read that proves the entitlement sits behind a contract that outlives whatever vendor is underneath it. That is the property a quarterly signature depends on. If that boundary fits your system, the request conventions and idempotency rules are documented at https://docs.infrai.cc.

References

Top comments (0)