Skip to content

feat: asymmetric signing for deployment-domain JWTs#54

Open
PenguinzTech wants to merge 1 commit into
docs/enterprise-hardeningfrom
feature/client-config-asymmetric-jwt
Open

feat: asymmetric signing for deployment-domain JWTs#54
PenguinzTech wants to merge 1 commit into
docs/enterprise-hardeningfrom
feature/client-config-asymmetric-jwt

Conversation

@PenguinzTech

@PenguinzTech PenguinzTech commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Stacked on #53 (docs/enterprise-hardening). Second branch in the chain.

What

Migrates ClientConfigManager deployment-domain tokens off HS256 (with a random per-process secret) onto the platform-wide asymmetric scheme:

  • Sign with the manager's private key (ES256 default, RS256 fallback); verify with the public key.
  • Algorithm allowlist is [ES256, RS256] only — HS256/none rejected (algorithm-confusion defense).
  • Standard iat/exp claims (PyJWT enforces expiry natively) + iss/aud.
  • ClientConfigManager.__init__ now takes private_key/public_key/algorithm/issuer/audience; app/__init__.py wires 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-exp reject, ES256 header assertion, ephemeral fallback. Full manager backend suite: 93 passing.

Upgrade note

Pre-v2.1.x domain tokens were HS256 — operators roll each domain over (rollover_domain_jwt) to re-issue under the asymmetric key. Documented in docs/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:

  • Support asymmetric ES256/RS256-signed deployment-domain tokens with issuer and audience claims in ClientConfigManager.
  • Allow configuration of JWT private/public keys, algorithm, issuer, and audience for client config management, with an ephemeral ES256 keypair fallback for dev/test.

Bug Fixes:

  • Ensure deployment-domain tokens remain verifiable across manager replicas and restarts by eliminating per-process random HS256 secrets.

Enhancements:

  • Restrict domain JWT verification to an ES256/RS256 algorithm allowlist and enforce standard iat/exp claims with issuer/audience validation.
  • Align deployment-domain token handling with the platform-wide JWT scheme and add logging when falling back to ephemeral keys.

Documentation:

  • Document the asymmetric deployment-domain token scheme, HS256 deprecation, and required token rollover in ENTERPRISE_SECURITY.md.

Tests:

  • Add regression tests covering asymmetric signing, header/claims correctness, HS256 and wrong-key rejection, expiry and missing-exp handling, and ephemeral keypair behavior for deployment-domain JWTs.

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>
@sourcery-ai

sourcery-ai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Reviewer's Guide

This 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 verification

sequenceDiagram
    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
Loading

File-Level Changes

Change Details Files
ClientConfigManager now signs and verifies deployment-domain JWTs using an asymmetric keypair with strict algorithm, issuer, audience, and claim handling.
  • Replaced jwt_secret HS256 signing with private_key/public_key fields and an algorithm setting constrained to ES256/RS256.
  • Added issuer and audience configuration to emitted tokens and verification, with PyJWT enforcing exp and iat via options.
  • Implemented a timezone-aware now for token issuance and one-year expiry using iat/exp instead of custom timestamps.
  • Adjusted domain JWT verification to use the public key, algorithm allowlist, and required exp/iat claims instead of manual expiry logic.
manager/backend/app/services/client_config_service.py
Introduced an ephemeral ES256 keypair generator used when no manager keypair is configured, and logged its limitations.
  • Added _generate_ephemeral_es256_keypair helper that produces PEM-encoded ES256 private/public keys using cryptography primitives.
  • Updated ClientConfigManager.init to fall back to an ephemeral keypair when private/public keys are absent and emit a warning about cross-replica validity.
  • Ensured default algorithm selection falls back to ES256 when an unsupported algorithm is provided.
manager/backend/app/services/client_config_service.py
Wired ClientConfigManager construction to the application’s JWT config so all replicas share the same asymmetric keypair and JWT metadata.
  • Updated Flask app factory to pass JWT_PRIVATE_KEY, JWT_PUBLIC_KEY, JWT_ALGORITHM, JWT_ISSUER, and JWT_AUDIENCE into ClientConfigManager.
  • Documented that deployment-domain tokens are now signed with the manager’s configured asymmetric keypair to remain valid across replicas and restarts.
manager/backend/app/__init__.py
Documented asymmetric deployment-domain token behavior and upgrade/rollover requirements in the enterprise security documentation.
  • Added a section describing deployment-domain tokens using the platform-wide asymmetric scheme with iss/aud and long-lived exp.
  • Explained that HS256 tokens pre-v2.1.x must be rolled over via Domain-Admin rollover_jwt/rollover_domain_jwt to switch to the asymmetric keypair.
  • Noted that the change fixes the latent multi-replica bug caused by per-process random HS256 secrets.
docs/ENTERPRISE_SECURITY.md
Added regression tests to validate asymmetric signing, verification semantics, and fallback behavior for deployment-domain JWTs using a lightweight fake DB.
  • Created helper to generate ES256 PEM keypairs for tests and a minimal fake penguin-dal setup (DB, tables, rows).
  • Added tests for header algorithm ES256, successful verification of valid tokens, rejection of wrong-key and HS256 tokens, and rejection of expired or missing-exp tokens.
  • Added a test asserting that an ephemeral keypair is generated and functions correctly when no explicit keypair is supplied to ClientConfigManager.
manager/backend/tests/test_client_config_domain_jwt.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +29 to +38
_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.
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant