feat: add tenant provisioning and key lifecycle (#5)
quality-gate / quality (push) Successful in 50s
quality-gate / quality (push) Successful in 50s
Closes #5
This commit is contained in:
@@ -9,6 +9,7 @@ decision requires a superseding ADR and corresponding contract/test updates.
|
||||
- [Threat model](../security/threat-model.md)
|
||||
- [Security promise and test matrix](../security/control-matrix.md)
|
||||
- [Versioned HTTP and UDP contracts](../contracts/README.md)
|
||||
- [Game provisioning and signing-key lifecycle](../security/provisioning.md)
|
||||
|
||||
These decisions intentionally leave gameplay authority, player identity,
|
||||
simulation, persistence, social features, skill matchmaking, and gameplay
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# Game provisioning and signing-key lifecycle
|
||||
|
||||
Tracking: #5
|
||||
|
||||
Rendezvous treats game and environment scope as provisioned policy, not caller
|
||||
input. Production starts only when it can build an enabled policy registry and
|
||||
load at least one currently active signing key from an external secret provider.
|
||||
Unknown and disabled scopes fail closed.
|
||||
|
||||
## Policy boundary
|
||||
|
||||
Each `GamePolicy` fixes the allowed:
|
||||
|
||||
- game/environment pair and regions;
|
||||
- exact gameplay protocol versions;
|
||||
- publisher trust and listing visibility modes;
|
||||
- metadata keys, required keys, per-value limits, total bytes, and key count;
|
||||
- listing, anonymous-host, and active-attempt quotas; and
|
||||
- dedicated fallback feature policy.
|
||||
|
||||
Publisher authorization first authenticates a typed principal, then derives the
|
||||
authoritative game/environment from that principal. Request fields are compared
|
||||
for mismatch detection but never replace the authenticated scope. Dedicated
|
||||
workloads, short-lived player-host grants, anonymous unlisted publishers, and
|
||||
operators are distinct principal types. Operator credentials cannot be used as
|
||||
publisher credentials, and anonymous publishers cannot escalate to public
|
||||
visibility.
|
||||
|
||||
## Signed credentials
|
||||
|
||||
Signed principal credentials use the compact form
|
||||
`rv1.<key-id>.<base64url-payload>.<base64url-HMAC-SHA256>`. The signed payload
|
||||
contains version, issuer, audience, subject, principal kind, bounded scope,
|
||||
issued/not-before/expiry times, and a random nonce. It contains no signing key,
|
||||
reusable publisher secret, player identity, or gameplay state.
|
||||
|
||||
Validation is deliberately ordered and bounded:
|
||||
|
||||
1. enforce the v1 opaque-credential length and four-segment grammar;
|
||||
2. resolve a known, non-revoked key in its verification window;
|
||||
3. compare the HMAC in fixed time;
|
||||
4. parse canonical bounded JSON;
|
||||
5. require exact version, issuer, and audience;
|
||||
6. enforce clock skew, expiry, key lifetime, principal kind, and scope shape.
|
||||
|
||||
Failures return typed internal reasons without echoing the credential. Logs and
|
||||
metrics must record only allowlisted tenant/principal/result dimensions; token,
|
||||
key, secret-reference value, and raw key material are excluded.
|
||||
|
||||
## Rotation and revocation
|
||||
|
||||
A key is bound either to operator credentials only or to allowed publisher
|
||||
credential kinds for exactly one game/environment. The verifier checks this
|
||||
authority after the signature, so even a compromised game grant issuer cannot
|
||||
mint a valid cross-game or operator credential.
|
||||
|
||||
A key also has three times: `NotBefore`, `SignUntil`, and `VerifyUntil`. Issuance
|
||||
picks the newest authorized non-revoked key inside its signing window. Older credentials continue
|
||||
to verify until the old key's verification window ends, providing an explicit
|
||||
overlap. After `VerifyUntil` they fail as retired. Configuration revocation and
|
||||
runtime revocation both reject immediately. A configured revoked key retains
|
||||
only its public key ID/lifecycle metadata and does not require retired secret
|
||||
material to remain available.
|
||||
|
||||
Key IDs are non-secret base64url identifiers. Secret references are resolved
|
||||
through `ISecretProvider`; production supports `env:<VARIABLE>` references and
|
||||
the interface is replaceable by a deployment-specific vault/KMS adapter. The
|
||||
committed development profile uses an in-memory random key identified by a
|
||||
`development:ephemeral/...` reference. It never writes key material to disk and
|
||||
all credentials become invalid when the process exits.
|
||||
|
||||
## Production configuration
|
||||
|
||||
`Rendezvous:Provisioning` supplies issuer, audience, clock skew, signing-key
|
||||
descriptors, and game policies. A production key reference such as
|
||||
`env:RENDEZVOUS_SIGNING_KEY_2026_01` expects that environment variable to hold at
|
||||
least 32 random bytes encoded as base64. Missing, malformed, short, inactive, or
|
||||
duplicate keys stop startup with a key-ID-only diagnostic. No game-wide secret
|
||||
belongs in `appsettings`, source control, examples, the Client package, URLs,
|
||||
responses, logs, metrics, exceptions, or diagnostic dumps.
|
||||
|
||||
Readiness becomes true only after provisioning and UDP startup both succeed.
|
||||
OpenAPI generation uses a pinned build-only host and does not start listeners or
|
||||
bypass provisioning in a deployed server process.
|
||||
Reference in New Issue
Block a user