Persist JWT signing key across server restarts #34

Closed
opened 2026-03-12 21:23:13 +01:00 by claude · 2 comments
Owner

Problem

The server generates an ephemeral JWT signing key on every startup. When the server restarts, all previously issued agent tokens (access + refresh) become invalid. While the agent now handles this via auto re-registration (cameleer3 commit 8f7ad27), this causes a brief disruption and unnecessary re-registration traffic for all connected agents.

Solution

Persist the JWT signing key so it survives server restarts:

  • Option A: Store in a K8s Secret, read on startup, generate only if not present
  • Option B: Store on a PVC-backed file path
  • Option C: Use a deterministic key derived from a stable secret (e.g., CAMELEER_AUTH_TOKEN + salt)

Option A is probably cleanest — generate once, store in Secret, load on subsequent starts.

Impact

Eliminates the root cause of agent reconnection storms after server restart. Agents keep working seamlessly across server restarts.

## Problem The server generates an ephemeral JWT signing key on every startup. When the server restarts, all previously issued agent tokens (access + refresh) become invalid. While the agent now handles this via auto re-registration (cameleer3 commit 8f7ad27), this causes a brief disruption and unnecessary re-registration traffic for all connected agents. ## Solution Persist the JWT signing key so it survives server restarts: - Option A: Store in a K8s Secret, read on startup, generate only if not present - Option B: Store on a PVC-backed file path - Option C: Use a deterministic key derived from a stable secret (e.g., `CAMELEER_AUTH_TOKEN` + salt) Option A is probably cleanest — generate once, store in Secret, load on subsequent starts. ## Impact Eliminates the root cause of agent reconnection storms after server restart. Agents keep working seamlessly across server restarts.
Author
Owner

Implementation Plan

Approach

Add optional config properties for pre-configured signing keys. When set, use them; when absent, generate random (backward compatible for dev).

Java Changes

  1. SecurityProperties.java — add jwtSecret, ed25519PrivateKey, ed25519PublicKey fields (all optional strings, base64-encoded)
  2. JwtServiceImpl.java — if jwtSecret is configured, decode and use it; otherwise generate random 256-bit key (current behavior)
  3. Ed25519SigningServiceImpl.java — if both ed25519PrivateKey and ed25519PublicKey are configured, reconstruct keypair from PKCS8/X509 DER; otherwise generate fresh keypair (current behavior)
  4. SecurityBeanConfig.java — pass SecurityProperties to Ed25519SigningServiceImpl constructor

Config

security:
  jwt-secret: ${CAMELEER_JWT_SECRET:}
  ed25519-private-key: ${CAMELEER_ED25519_PRIVATE_KEY:}
  ed25519-public-key: ${CAMELEER_ED25519_PUBLIC_KEY:}

K8s/CI

  • New secret cameleer-signing-keys with 3 keys, created idempotently in CI deploy step
  • deploy/server.yaml references via secretKeyRef
  • 3 new Gitea CI secrets: CAMELEER_JWT_SECRET, CAMELEER_ED25519_PRIVATE_KEY, CAMELEER_ED25519_PUBLIC_KEY

Key Generation (one-time)

// jshell
byte[] s = new byte[32]; new java.security.SecureRandom().nextBytes(s);
System.out.println(java.util.Base64.getEncoder().encodeToString(s));
var kp = java.security.KeyPairGenerator.getInstance("Ed25519").generateKeyPair();
System.out.println(java.util.Base64.getEncoder().encodeToString(kp.getPrivate().getEncoded()));
System.out.println(java.util.Base64.getEncoder().encodeToString(kp.getPublic().getEncoded()));

Note

First deployment with persistent Ed25519 keys means agents with the old ephemeral key must re-register once.

## Implementation Plan ### Approach Add optional config properties for pre-configured signing keys. When set, use them; when absent, generate random (backward compatible for dev). ### Java Changes 1. **`SecurityProperties.java`** — add `jwtSecret`, `ed25519PrivateKey`, `ed25519PublicKey` fields (all optional strings, base64-encoded) 2. **`JwtServiceImpl.java`** — if `jwtSecret` is configured, decode and use it; otherwise generate random 256-bit key (current behavior) 3. **`Ed25519SigningServiceImpl.java`** — if both `ed25519PrivateKey` and `ed25519PublicKey` are configured, reconstruct keypair from PKCS8/X509 DER; otherwise generate fresh keypair (current behavior) 4. **`SecurityBeanConfig.java`** — pass `SecurityProperties` to `Ed25519SigningServiceImpl` constructor ### Config ```yaml security: jwt-secret: ${CAMELEER_JWT_SECRET:} ed25519-private-key: ${CAMELEER_ED25519_PRIVATE_KEY:} ed25519-public-key: ${CAMELEER_ED25519_PUBLIC_KEY:} ``` ### K8s/CI - New secret `cameleer-signing-keys` with 3 keys, created idempotently in CI deploy step - `deploy/server.yaml` references via `secretKeyRef` - 3 new Gitea CI secrets: `CAMELEER_JWT_SECRET`, `CAMELEER_ED25519_PRIVATE_KEY`, `CAMELEER_ED25519_PUBLIC_KEY` ### Key Generation (one-time) ```java // jshell byte[] s = new byte[32]; new java.security.SecureRandom().nextBytes(s); System.out.println(java.util.Base64.getEncoder().encodeToString(s)); var kp = java.security.KeyPairGenerator.getInstance("Ed25519").generateKeyPair(); System.out.println(java.util.Base64.getEncoder().encodeToString(kp.getPrivate().getEncoded())); System.out.println(java.util.Base64.getEncoder().encodeToString(kp.getPublic().getEncoded())); ``` ### Note First deployment with persistent Ed25519 keys means agents with the old ephemeral key must re-register once.
Author
Owner

Resolved in a4de2a7. JWT secret is now configurable via CAMELEER_JWT_SECRET env var. If set, tokens survive restarts. Falls back to random secret (current behavior) if not set. Implemented as part of the RBAC + OIDC support commit.

Resolved in a4de2a7. JWT secret is now configurable via `CAMELEER_JWT_SECRET` env var. If set, tokens survive restarts. Falls back to random secret (current behavior) if not set. Implemented as part of the RBAC + OIDC support commit.
Sign in to join this conversation.