feat: asymmetric signing for deployment-domain JWTs#54
Conversation
Migrate ClientConfigManager deployment-domain tokens from HS256 with a random per-process secret to the platform-wide asymmetric scheme: - Sign with the manager's private key (ES256 default, RS256 fallback); verify with the public key; algorithms allowlist is [ES256, RS256] only (HS256/none rejected — algorithm-confusion defense). - Use standard iat/exp (PyJWT enforces expiry natively) plus iss/aud. - ClientConfigManager takes private_key/public_key/algorithm/issuer/ audience; app wires them from the manager JWT config. Falls back to an ephemeral ES256 keypair only when unconfigured (dev/test). Also fixes a latent multi-replica bug: the old random per-process secret made domain tokens unverifiable across replicas/restarts. Adds 7 regression tests (valid accept, wrong-key reject, HS256 reject, expired reject, missing-exp reject, ES256 header, ephemeral fallback). Full manager suite: 93 passing. Documents the upgrade/rollover note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewer's GuideThis PR migrates deployment-domain JWTs in ClientConfigManager from a per-process HS256 secret to the platform’s asymmetric JWT scheme (ES256/RS256), wires the manager’s keypair via app config, hardens verification semantics, and documents the upgrade path and behavior, backed by targeted regression tests. Sequence diagram for asymmetric deployment-domain JWT issuance and verificationsequenceDiagram
participant ManagerApp
participant ClientConfigManager
participant PyJWT
ManagerApp->>ClientConfigManager: __init__(db_url, private_key, public_key, algorithm, issuer, audience)
ClientConfigManager->>ClientConfigManager: _generate_ephemeral_es256_keypair() [if private_key or public_key missing]
ClientConfigManager->>ClientConfigManager: set private_key, public_key, algorithm, issuer, audience
ManagerApp->>ClientConfigManager: _generate_domain_jwt(domain_name)
ClientConfigManager->>ClientConfigManager: build payload with domain, iss, aud, iat, exp
ClientConfigManager->>PyJWT: encode(payload, private_key, algorithm)
PyJWT-->>ClientConfigManager: jwt_token
ManagerApp->>ClientConfigManager: _verify_domain_jwt(jwt_token)
ClientConfigManager->>PyJWT: decode(jwt_token, public_key, algorithms=[ES256, RS256], issuer, audience, options={require: [exp, iat]})
PyJWT-->>ClientConfigManager: payload [or raises error]
ClientConfigManager->>ClientConfigManager: validate domain in payload
ClientConfigManager-->>ManagerApp: domain info or None
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- The
algorithmparameter inClientConfigManager.__init__is silently coerced toES256when not in_DOMAIN_JWT_ALGORITHMS; consider raising a configuration error instead so misconfigurations are surfaced early. - Ephemeral ES256 keypair generation is embedded inside
ClientConfigManager.__init__; you may want to extract this into a separate helper or config path so production and dev/test key handling are more clearly separated and easier to reason about.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `algorithm` parameter in `ClientConfigManager.__init__` is silently coerced to `ES256` when not in `_DOMAIN_JWT_ALGORITHMS`; consider raising a configuration error instead so misconfigurations are surfaced early.
- Ephemeral ES256 keypair generation is embedded inside `ClientConfigManager.__init__`; you may want to extract this into a separate helper or config path so production and dev/test key handling are more clearly separated and easier to reason about.
## Individual Comments
### Comment 1
<location path="manager/backend/app/services/client_config_service.py" line_range="29-38" />
<code_context>
+# Verifiers accept asymmetric algorithms only. HS256/none are rejected to block
+# the public-key-as-HMAC algorithm-confusion attack, consistent with the rest of
+# the platform (dns-server/dhcp-server/ntp-server verifiers).
+_DOMAIN_JWT_ALGORITHMS = ["ES256", "RS256"]
+
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Ephemeral keypair generation can mismatch with `RS256` algorithm, leading to signing/verification errors.
If no keypair is provided, `_generate_ephemeral_es256_keypair` always returns an EC key for ES256, but `self.algorithm` may still be set to `RS256`. PyJWT will then fail at runtime because the key type does not match the algorithm. Ensure the algorithm is forced to `ES256` when using the ephemeral EC keypair, or generate an RSA keypair when `algorithm == "RS256"` so key type and algorithm always align.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| _DOMAIN_JWT_ALGORITHMS = ["ES256", "RS256"] | ||
|
|
||
|
|
||
| def _generate_ephemeral_es256_keypair() -> tuple[str, str]: | ||
| """Generate an in-process ES256 keypair (PEM strings). | ||
|
|
||
| Used only when no manager keypair is supplied (standalone/dev/test). In a | ||
| real deployment the manager passes its configured JWT_PRIVATE_KEY / | ||
| JWT_PUBLIC_KEY so domain tokens are verifiable across replicas and restarts. | ||
| """ |
There was a problem hiding this comment.
issue (bug_risk): Ephemeral keypair generation can mismatch with RS256 algorithm, leading to signing/verification errors.
If no keypair is provided, _generate_ephemeral_es256_keypair always returns an EC key for ES256, but self.algorithm may still be set to RS256. PyJWT will then fail at runtime because the key type does not match the algorithm. Ensure the algorithm is forced to ES256 when using the ephemeral EC keypair, or generate an RSA keypair when algorithm == "RS256" so key type and algorithm always align.
Stacked on #53 (docs/enterprise-hardening). Second branch in the chain.
What
Migrates
ClientConfigManagerdeployment-domain tokens off HS256 (with a random per-process secret) onto the platform-wide asymmetric scheme:[ES256, RS256]only — HS256/nonerejected (algorithm-confusion defense).iat/expclaims (PyJWT enforces expiry natively) +iss/aud.ClientConfigManager.__init__now takesprivate_key/public_key/algorithm/issuer/audience;app/__init__.pywires them from the manager JWT config. Ephemeral ES256 keypair only when unconfigured (dev/test).Also fixes
A latent multi-replica bug: the old
secrets.token_urlsafe(32)per-process secret made domain tokens unverifiable across manager replicas/restarts.Tests
7 new regression tests (
test_client_config_domain_jwt.py): valid accept, wrong-key reject, HS256 reject, expired reject, missing-expreject, ES256 header assertion, ephemeral fallback. Full manager backend suite: 93 passing.Upgrade note
Pre-
v2.1.xdomain tokens were HS256 — operators roll each domain over (rollover_domain_jwt) to re-issue under the asymmetric key. Documented indocs/ENTERPRISE_SECURITY.md.🤖 Generated with Claude Code
Summary by Sourcery
Migrate deployment-domain JWTs in ClientConfigManager to the platform’s asymmetric signing scheme and wire them to the manager’s configured keypair.
New Features:
Bug Fixes:
Enhancements:
Documentation:
Tests: