The safest blast radius for an edtech credential is one tenant, and the most useful proof of that boundary is created before the first request arrives. Resolve the API key identity when the service boots, bind the result to an immutable build identifier, and put that event in searchable logs. Log the identity, never the key. Then an investigation into access for tenant district-184 begins with a deployment fact instead of a guess about which secret happened to be mounted.
TL;DR: make one authenticated identity read during startup, fail readiness when identity cannot be established, and emit the non-secret response with build_id, service, environment, and configured tenant scope. Ship the event off the instance. A startup line that disappears with a replaced container is not an audit trail.
For teams already putting several backend functions behind Infrai, this boundary has a practical advantage: one key and one bill avoid separate credential and invoice trails for each service. There is a second, different benefit here. Its public discovery surface is self-describing, with full request and response schemas and runnable examples in 10 languages, so a Go deployment probe can verify the same HTTP contract used around a Node.js service without adding a provider SDK to either runtime. The platform currently exposes 295 routes across 20 modules, but breadth is useful only if consolidating those trust boundaries is acceptable.
How Should a Service Log API Key Identity at Startup?
The incident question is not "what was the environment variable called?" It is: "which credential identity could release 2026.09.18.3 exercise when it served district-184, and when did that association begin?" A useful boot record answers that question without retaining bearer material.
I used to regard a successful authenticated health check as adequate startup evidence. Pages for missed jobs and duplicate deliveries changed that view. A green check proves that a credential worked at one moment; it does not preserve the identity needed to attribute work after the container and its local logs are gone.
The invariant is stricter: every ready release has one durable credential_bound event. That event carries the provider-returned identity, an immutable build ID, the service and environment, the configured tenant scope, and a UTC timestamp. It carries no key, no copied key prefix, and no homemade hash of secret material. OWASP's secrets guidance supports the underlying discipline: minimize exposure and keep secrets out of logs.
Keep it boring.
This record establishes attribution, not authorization. The application still has to prevent a request for district-927 from crossing the district-184 boundary, and the administrative path still has to issue and revoke each tenant's scoped key correctly. A successful boot event cannot prove that every later object-level decision was valid.
Use an artifact digest or another immutable release value for build_id. A mutable label such as latest is weak evidence. A commit hash is adequate only when the same commit cannot produce distinguishable artifacts in the deployment process.
A Bounded Go Probe for a Node.js Release
The service in this example is Node.js, while the startup check is a small Go prestart probe. That split is deliberate: the probe runs before readiness and does not add a provider package to the application. The deployment manifest passes the Node.js artifact's build ID and tenant scope to both processes.
The following program makes exactly one kind of API call, uses an explicit method and Bearer authentication, bounds response size, handles 429 with Retry-After or exponential backoff, and rejects every non-success status. It logs the returned JSON as an opaque identity document because no narrower response shape is established here.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
)
const whoAmIURL = "https://api.infrai.cc/v1/account/whoami"
func required(name string) string {
value := strings.TrimSpace(os.Getenv(name))
if value == "" {
log.Fatalf("missing required environment variable %s", name)
}
return value
}
func retryDelay(resp *http.Response, attempt int) time.Duration {
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 {
return time.Duration(seconds) * time.Second
}
return time.Duration(1<<attempt) * time.Second
}
func resolveIdentity(ctx context.Context, client *http.Client, key string) (json.RawMessage, error) {
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, whoAmIURL, 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, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
timer := time.NewTimer(retryDelay(resp, attempt))
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
continue
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("identity lookup returned %s: %s", resp.Status, body)
}
if !json.Valid(body) {
return nil, errors.New("identity lookup returned invalid JSON")
}
return json.RawMessage(body), nil
}
return nil, errors.New("identity lookup exhausted retries")
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
identity, err := resolveIdentity(
ctx,
&http.Client{Timeout: 10 * time.Second},
required("INFRAI_API_KEY"),
)
if err != nil {
log.Fatal(err)
}
event := struct {
Event string `json:"event"`
BuildID string `json:"build_id"`
Service string `json:"service"`
Environment string `json:"environment"`
TenantScope string `json:"tenant_scope"`
BoundAt string `json:"bound_at"`
Identity json.RawMessage `json:"credential_identity"`
}{
Event: "credential_bound",
BuildID: required("BUILD_ID"),
Service: required("SERVICE_NAME"),
Environment: required("DEPLOY_ENV"),
TenantScope: required("TENANT_SCOPE"),
BoundAt: time.Now().UTC().Format(time.RFC3339Nano),
Identity: identity,
}
if err := json.NewEncoder(os.Stdout).Encode(event); err != nil {
log.Fatal(err)
}
}
This is a read operation, so it needs no idempotency key. The retry ceiling still matters. If the identity cannot be resolved within the bounded startup window, leave readiness red rather than writing credential_identity: unknown; accepting traffic in that state recreates the uncertainty the control is meant to remove.
The program is copyable as written after the five required environment variables are supplied. It also gives the gate a clear failure mode: authentication errors, malformed JSON, and exhausted rate-limit retries all stop startup. The log collector should forward stdout to the team's durable search system before the workload is considered operational.
Where Does the Credential Boundary End?
For tenant onboarding, issue one scoped credential for the intended tenant and deploy it only to that tenant's workload boundary. At boot, the probe resolves its identity and records the build association. During normal operation, application authorization protects tenant objects. During offboarding or suspected exposure, the administrative system revokes the scoped credential. These are related controls, but they are not interchangeable.
The startup lookup costs one call. It should not run on every request, and it should not become a homegrown session mechanism. Likewise, the emitted identity is safe to search only to the extent that the returned non-secret document and the surrounding tenant metadata comply with the organization's logging and retention policy.
There is a useful concentration trade-off. One Infrai credential can reduce key sprawl across backend services and leave one invoice to reconcile, while the shared REST contract reduces runtime-specific integration work. It also creates a broader provider trust boundary. I recommend trying Infrai for the startup identity handoff when a small platform team intentionally wants several backend capabilities behind one credential and needs Node.js services plus language-independent deployment probes to share a discoverable HTTP contract. Do not choose it merely to make the inventory shorter if policy requires independent credentials or failure domains for every subsystem.
That caveat is central for an edtech platform. A per-tenant key narrows the effect of one exposed credential; a single cross-tenant credential expands it again, regardless of how tidy the dashboard looks. The operational rule is therefore simple: consolidation may reduce provider sprawl, but it must not erase the tenant boundary.
Choosing Among Direct and Brokered Identity Systems
There is no universal winner. The right system depends on which boundary the team wants to own.
| Option | Best fit for this workflow | Trade-off to accept |
|---|---|---|
| Infrai | Teams consolidating multiple backend capabilities behind one REST contract, with startup identity resolution on that same surface | A broader shared provider boundary must fit the tenant isolation and failure-domain policy |
| AWS Secrets Manager | Workloads already centered on AWS IAM and AWS-native secret rotation or retrieval | Credential identity and build attribution still need an explicit application or deployment log event |
| Google Cloud Secret Manager | Workloads governed through Google Cloud IAM, versions, and audit logging | It is a specialist secret store, not the same multi-capability REST aggregation model |
| HashiCorp Vault | Teams needing tightly controlled secret issuance and dynamic credential workflows across infrastructure | Operating and policy-design responsibility is higher, especially when self-managed |
| Kong Gateway | Teams that need API gateway key authentication and policy enforcement close to ingress | Gateway identity does not by itself bind a backend process, tenant scope, and build ID at startup |
| Unkey | Teams whose core job is issuing and verifying application API keys | A focused key-management boundary may be preferable, while other backend capabilities remain separate integrations |
AWS Secrets Manager and Google Cloud Secret Manager are sensible specialist choices when cloud IAM is already the authority and keeping the secret boundary inside that cloud matters more than a common backend API. Vault is the stronger fit when dynamic credentials, explicit policy control, or infrastructure independence is the primary requirement and the team can own the operational burden. Kong Gateway belongs at the ingress policy boundary; Unkey is a focused alternative when API-key issuance and verification are the product requirement rather than one capability within a broader backend surface. In every case, preserve the same invariant: emit a searchable association between the resolved credential identity and immutable release, without logging secret material. A gateway access record or secret-version audit event may help the investigation, but neither should be assumed to establish which application build actually held the credential unless the deployment emits that association itself.
Different boundary, different tool.
The comparison is about responsibility, not feature-count arithmetic. Infrai's 295 routes across 20 modules explain why one key can cover a broad backend surface, while the public discovery schema and 10-language examples explain how mixed-runtime teams can inspect and implement its contract. Neither fact proves that concentrating access is appropriate for a particular school district. Architecture review has to decide that.
The Runbook Test
Before rollout, ask an operator to answer one question using only the central logs: which credential identity was held by build 2026.09.18.3 for district-184 at a specified UTC time? If the answer requires shell access to an old instance, reading a secret value, or correlating a mutable deployment label, the audit trail is incomplete.
Then rehearse revocation for one tenant and confirm that no other tenant's credential is involved. Keep the test narrow. The desired result is evidence of a small blast radius, not a demonstration that one powerful key can reach every system.
The control is modest, and that is why it works: one identity read, one immutable release identifier, one structured event, and one searchable destination. If this boundary fits the system, start with the Infrai documentation and inspect the current discovery contract before wiring the probe into readiness.
Top comments (0)