Files
cameleer-server/.planning/phases/04-security/04-RESEARCH.md
hsiegeln cb3ebfea7c
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 18s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
chore: rename cameleer3 to cameleer
Rename Java packages from com.cameleer3 to com.cameleer, module
directories from cameleer3-* to cameleer-*, and all references
throughout workflows, Dockerfiles, docs, migrations, and pom.xml.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:28:42 +02:00

31 KiB

Phase 4: Security - Research

Researched: 2026-03-11 Domain: Spring Security JWT authentication, Ed25519 signing, bootstrap token validation Confidence: HIGH

Summary

This phase adds authentication and integrity protection to the Cameleer server. The implementation uses Spring Security 6.4.3 (managed by Spring Boot 3.4.3) with a custom OncePerRequestFilter for JWT validation, JDK 17 built-in Ed25519 for signing SSE payloads, and environment variable-based bootstrap tokens for agent registration. The approach is deliberately simple -- no OAuth2 resource server, no external identity provider, just symmetric HMAC JWTs for access control and Ed25519 signatures for payload integrity.

The existing codebase has clear integration points: AgentRegistrationController.register() already returns serverPublicKey: null as a placeholder, SseConnectionManager.onCommandReady() is the signing hook for SSE events, and WebConfig already defines excluded paths that align with the public endpoint list. Spring Security's SecurityFilterChain replaces the need for hand-rolled authorization logic -- endpoints are protected by default, with explicit permitAll() for health, register, and docs.

Primary recommendation: Use Nimbus JOSE+JWT (transitive via spring-boot-starter-security) with HMAC-SHA256 for JWTs, JDK built-in KeyPairGenerator.getInstance("Ed25519") for signing keypairs, and a single SecurityFilterChain bean with a custom JwtAuthenticationFilter extends OncePerRequestFilter added before UsernamePasswordAuthenticationFilter.

<user_constraints>

User Constraints (from CONTEXT.md)

Locked Decisions

  • Single shared token from CAMELEER_AUTH_TOKEN env var -- no config file fallback
  • Agent passes bootstrap token via Authorization: Bearer <token> header on POST /register
  • Server returns 401 Unauthorized when token is missing or invalid -- no detail about what's wrong
  • Server fails fast on startup if CAMELEER_AUTH_TOKEN is not set -- prevents running insecure
  • Hot rotation via dual-token overlap: support CAMELEER_AUTH_TOKEN_PREVIOUS env var, server accepts both during rotation window
  • Access JWT expires after 1 hour
  • Separate refresh token with 7-day expiry, issued alongside access JWT at registration
  • Agent calls POST /api/v1/agents/{id}/refresh with refresh token to get new access JWT
  • JWT claims: sub = agentId, custom claim for group
  • Registration response includes both access JWT and refresh token (replaces current serverPublicKey: null placeholder with actual public key)
  • Ephemeral keypair generated fresh each server startup -- no persistence needed
  • Agents receive public key during registration; must re-register after server restart to get new key
  • Signature included as a signature field in the SSE event data JSON -- agent verifies payload minus signature field
  • All command types signed (config-update, deep-trace, replay) -- uniform security model
  • Public (no JWT): GET /health, POST /register (uses bootstrap token), OpenAPI/Swagger UI docs
  • Protected (JWT required): all other endpoints including ingestion (/data/**), search, agent management, commands
  • SSE connections authenticated via JWT as query parameter: /agents/{id}/events?token=<jwt> (EventSource API doesn't support custom headers)
  • Spring Security filter chain (spring-boot-starter-security) with custom JwtAuthenticationFilter

Claude's Discretion

  • JWT signing algorithm (HMAC with server secret vs Ed25519 for JWT too)
  • Nimbus JOSE+JWT vs jjwt vs other JWT library
  • Ed25519 implementation library (Bouncy Castle vs JDK built-in)
  • Spring Security configuration details (SecurityFilterChain bean, permit patterns)
  • Refresh token storage mechanism (in-memory map, agent registry, or stateless)

Deferred Ideas (OUT OF SCOPE)

  • User/UI authentication -- belongs with web UI in v2
  • Role-based access control (admin vs agent vs viewer) -- future phase
  • Token revocation list -- evaluate after v1 usage patterns
  • Mutual TLS as additional transport security -- infrastructure concern, not application layer
  • Key rotation API endpoint -- adds attack surface, stick with restart-based rotation for v1 </user_constraints>

<phase_requirements>

Phase Requirements

ID Description Research Support
SECU-01 (#23) All API endpoints (except health and register) require valid JWT Bearer token Spring Security SecurityFilterChain with permitAll() for public paths, custom JwtAuthenticationFilter for JWT validation
SECU-02 (#24) JWT refresh flow via POST /api/v1/agents/{id}/refresh Nimbus JOSE+JWT for JWT creation/validation, stateless refresh tokens with longer expiry
SECU-03 (#25) Server generates Ed25519 keypair; public key delivered at registration JDK 17 built-in KeyPairGenerator.getInstance("Ed25519"), Base64-encoded public key in registration response
SECU-04 (#26) All config-update and replay SSE payloads signed with Ed25519 private key JDK 17 Signature.getInstance("Ed25519"), signing hook in SseConnectionManager.onCommandReady()
SECU-05 (#27) Bootstrap token from CAMELEER_AUTH_TOKEN env var validates initial agent registration @Value injection with startup validation, checked before registration logic
</phase_requirements>

Standard Stack

Core

Library Version Purpose Why Standard
spring-boot-starter-security 3.4.3 (managed) Security filter chain, authentication framework Spring Boot's standard security starter; brings Spring Security 6.4.3
nimbus-jose-jwt 9.37+ (transitive via spring-security-oauth2-jose) JWT creation, signing, parsing, verification Spring Security's own JWT library; already in the Spring ecosystem
JDK Ed25519 JDK 17 built-in Ed25519 keypair generation and signing Native support since Java 15 via java.security.KeyPairGenerator and java.security.Signature; no external dependency needed

Supporting

Library Version Purpose When to Use
spring-boot-starter-test 3.4.3 (managed) MockMvc with security context, @WithMockUser support Already present; tests gain security testing support automatically

Alternatives Considered

Instead of Could Use Tradeoff
Nimbus JOSE+JWT JJWT (io.jsonwebtoken) JJWT is simpler API but doesn't support JWE; Nimbus is already a Spring Security transitive dependency so adding it explicitly costs zero
JDK Ed25519 Bouncy Castle Bouncy Castle adds ~5MB dependency for something JDK 17 does natively; only needed if targeting Java < 15
HMAC-SHA256 for JWT Ed25519 for JWT too HMAC is simpler for server-only JWT creation/validation (no key distribution needed); Ed25519 for JWT only matters if a third party validates JWTs

Discretion Recommendations:

  • JWT signing algorithm: Use HMAC-SHA256 (HS256). The server both creates and validates JWTs -- no external party needs to verify them. HMAC is simpler (one shared secret vs keypair), and the 256-bit secret can be generated randomly at startup (ephemeral, like the Ed25519 key). This keeps JWT signing separate from Ed25519 payload signing -- cleaner separation of concerns.
  • JWT library: Use Nimbus JOSE+JWT. It is Spring Security's transitive dependency, so it costs nothing extra. Adding spring-boot-starter-security brings spring-security-oauth2-jose which includes Nimbus. Alternatively, add com.nimbusds:nimbus-jose-jwt directly if not pulling the full OAuth2 stack.
  • Ed25519 library: Use JDK built-in. Zero external dependencies, native performance, well-tested in JDK 17+.
  • Refresh token storage: Use stateless signed refresh tokens (also HMAC-signed JWTs with different claims/expiry). This avoids any in-memory storage for refresh tokens and scales naturally. The refresh token is just a JWT with type=refresh, sub=agentId, and 7-day expiry. On refresh, validate the refresh JWT, check agent still exists, issue new access JWT.

Installation (add to cameleer-server-app pom.xml):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus-jose-jwt</artifactId>
    <version>9.47</version>
</dependency>

Note: If spring-boot-starter-security brings Nimbus transitively (via spring-security-oauth2-jose), the explicit Nimbus dependency is optional. However, since we are NOT using Spring Security's OAuth2 resource server (we have a custom filter), adding Nimbus explicitly ensures it is available. Check if the starter alone suffices; if not, add Nimbus directly.

Architecture Patterns

cameleer-server-core/src/main/java/com/cameleer/server/core/
  security/
    JwtService.java              # Interface: createAccessToken, createRefreshToken, validateToken, extractAgentId
    Ed25519SigningService.java    # Interface: sign(payload) -> signature, getPublicKeyBase64()

cameleer-server-app/src/main/java/com/cameleer/server/app/
  security/
    JwtServiceImpl.java          # Nimbus JOSE+JWT HMAC implementation
    Ed25519SigningServiceImpl.java # JDK Ed25519 keypair + signing implementation
    JwtAuthenticationFilter.java # OncePerRequestFilter: extract JWT, validate, set SecurityContext
    BootstrapTokenValidator.java # Validates bootstrap token(s) from env vars
    SecurityConfig.java          # SecurityFilterChain bean, permit patterns
  config/
    SecurityProperties.java      # @ConfigurationProperties for token expiry, etc.

Pattern 1: SecurityFilterChain with Custom JWT Filter

What: Single SecurityFilterChain bean that permits public paths and requires authentication everywhere else, with a custom JwtAuthenticationFilter added before Spring's UsernamePasswordAuthenticationFilter. When to use: Always -- this is the sole security configuration. Example:

// Source: Spring Security 6.4 official docs + Spring Boot 3.4 patterns
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http,
                                                    JwtAuthenticationFilter jwtFilter) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // REST API, no browser forms
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/health").permitAll()
                .requestMatchers("/api/v1/agents/register").permitAll()
                .requestMatchers("/api/v1/api-docs/**").permitAll()
                .requestMatchers("/api/v1/swagger-ui/**").permitAll()
                .requestMatchers("/swagger-ui/**").permitAll()
                .requestMatchers("/v3/api-docs/**").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

Pattern 2: JwtAuthenticationFilter (OncePerRequestFilter)

What: Extracts JWT from Authorization: Bearer <token> header (or token query param for SSE), validates it, and sets a Spring Security Authentication object in the SecurityContextHolder. When to use: Every authenticated request. Example:

// Custom filter pattern for Spring Security 6.x
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final AgentRegistryService agentRegistry;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                     HttpServletResponse response,
                                     FilterChain chain) throws ServletException, IOException {
        String token = extractToken(request);
        if (token != null) {
            try {
                String agentId = jwtService.validateAndExtractAgentId(token);
                // Verify agent still exists
                if (agentRegistry.findById(agentId) != null) {
                    var auth = new UsernamePasswordAuthenticationToken(
                            agentId, null, List.of());
                    SecurityContextHolder.getContext().setAuthentication(auth);
                }
            } catch (Exception e) {
                // Invalid token -- do not set authentication, Spring Security will reject
            }
        }
        chain.doFilter(request, response);
    }

    private String extractToken(HttpServletRequest request) {
        // 1. Check Authorization header
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            return header.substring(7);
        }
        // 2. Check query parameter (for SSE EventSource)
        return request.getParameter("token");
    }
}

Pattern 3: Ed25519 Payload Signing in SSE Delivery

What: Before sending an SSE event, serialize the payload JSON, compute an Ed25519 signature, add the signature field to the JSON, then send. When to use: Every SSE command delivery (config-update, deep-trace, replay). Example:

// JDK 17 built-in Ed25519 signing
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("Ed25519");
KeyPair keyPair = keyGen.generateKeyPair();

// Signing
Signature signer = Signature.getInstance("Ed25519");
signer.initSign(keyPair.getPrivate());
signer.update(payloadJson.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = signer.sign();
String signatureBase64 = Base64.getEncoder().encodeToString(signatureBytes);

// Public key for registration response
String publicKeyBase64 = Base64.getEncoder().encodeToString(
    keyPair.getPublic().getEncoded()); // X.509 SubjectPublicKeyInfo DER encoding

Pattern 4: Bootstrap Token Validation

What: Check Authorization: Bearer <token> on POST /register against CAMELEER_AUTH_TOKEN (and optionally CAMELEER_AUTH_TOKEN_PREVIOUS). When to use: Only on the registration endpoint. Example:

// Startup validation in a @Component or @Bean init
@Value("${CAMELEER_AUTH_TOKEN:#{null}}")
private String bootstrapToken;

@PostConstruct
void validateBootstrapToken() {
    if (bootstrapToken == null || bootstrapToken.isBlank()) {
        throw new IllegalStateException(
            "CAMELEER_AUTH_TOKEN environment variable must be set");
    }
}

Anti-Patterns to Avoid

  • Registering JwtAuthenticationFilter as a @Bean without @Component exclusion: If marked as @Component, Spring Boot will register it as a global servlet filter AND in the security chain, running it twice. Either do NOT annotate it as @Component (construct it manually in the SecurityConfig bean) or use FilterRegistrationBean to disable auto-registration.
  • Checking JWT on every request including permitAll paths: The filter runs on all requests, but should gracefully skip validation for public paths (just call chain.doFilter if no token present -- Spring Security's authorization rules handle the rest).
  • Storing refresh tokens in-memory: Unnecessarily complex and lost on restart. Stateless signed refresh tokens are sufficient.
  • Using Ed25519 for JWT signing: Adds complexity (key distribution, asymmetric operations) for no benefit when only the server creates and validates JWTs.

Don't Hand-Roll

Problem Don't Build Use Instead Why
JWT creation/validation Custom token format or Base64 JSON Nimbus JOSE+JWT SignedJWT + MACSigner/MACVerifier Handles algorithm validation, claim parsing, expiry checks, type-safe builders
Request authentication Custom servlet filter checking headers manually Spring Security SecurityFilterChain + OncePerRequestFilter Handles CORS, CSRF disabling, session management, exception handling, path matching
Ed25519 signing Hand-rolled crypto or custom signature format JDK java.security.Signature + java.security.KeyPairGenerator Audited, constant-time, handles DER encoding properly
Constant-time token comparison String.equals() for bootstrap token MessageDigest.isEqual() Prevents timing attacks on bootstrap token validation
Public key encoding Custom byte formatting PublicKey.getEncoded() + Base64 Standard X.509 SubjectPublicKeyInfo DER format, interoperable with any Ed25519 library

Key insight: Cryptographic code has an extraordinary surface area for subtle bugs (timing attacks, encoding mismatches, algorithm confusion). Every piece should use battle-tested library methods.

Common Pitfalls

Pitfall 1: Double Filter Registration

What goes wrong: Annotating JwtAuthenticationFilter with @Component causes Spring Boot to auto-register it as a global servlet filter AND Spring Security adds it to the filter chain, resulting in the filter executing twice per request. Why it happens: Spring Boot auto-detects @Component classes that extend Filter and registers them globally. How to avoid: Do NOT annotate the filter as @Component. Instead, construct it in SecurityConfig and pass it to addFilterBefore(). If you must use @Component, add a FilterRegistrationBean that disables auto-registration. Warning signs: Filter logging messages appear twice per request; 401 responses on valid tokens (filter runs before SecurityFilterChain on second pass).

Pitfall 2: Spring Security Blocking Existing Tests

What goes wrong: Adding spring-boot-starter-security immediately makes all endpoints return 401/403 in existing integration tests. Why it happens: Spring Security's default configuration denies all requests. Existing tests don't include JWT tokens. How to avoid: Two approaches: (1) Add @WithMockUser or test-specific security configuration for existing tests, or (2) set a test-profile application-test.yml property with a known bootstrap token and have test helpers generate valid JWTs. Prefer option (2) for realistic security testing. Warning signs: All existing ITs start failing with 401 after adding the security starter.

Pitfall 3: SSE Token in URL Logged/Cached

What goes wrong: JWT passed as query parameter ?token=<jwt> appears in server access logs, proxy logs, and browser history. Why it happens: Query parameters are part of the URL, which is logged by default. How to avoid: Use short-lived access JWTs (1 hour is fine). Consider filtering the token parameter from access logs. The EventSource API limitation makes this unavoidable -- document it as a known tradeoff. Warning signs: JWT tokens visible in plain text in log files.

Pitfall 4: Timing Attack on Bootstrap Token

What goes wrong: Using String.equals() for bootstrap token comparison leaks token length/prefix via timing side-channel. Why it happens: String.equals() short-circuits on first mismatch. How to avoid: Use MessageDigest.isEqual(a.getBytes(), b.getBytes()) for constant-time comparison. Warning signs: None visible in normal operation -- this is a preventive measure.

Pitfall 5: Ed25519 Signature Field Ordering

What goes wrong: Agent cannot verify signature because JSON field ordering differs between signing and verification. Why it happens: JSON object field order is not guaranteed. If the server signs a different serialization than the agent verifies, signatures won't match. How to avoid: Sign the JSON payload WITHOUT the signature field (sign the payload as-is before adding the signature). Document clearly: "signature is computed over the data field value of the SSE event, excluding the signature key". Use a canonical approach: sign the payload JSON string, then wrap it in an outer object with data and signature fields. Warning signs: Signature verification fails intermittently or consistently on the agent side.

Pitfall 6: Forgetting to Exclude Actuator/Springdoc Paths

What goes wrong: Health endpoint returns 401 because the SecurityFilterChain doesn't match the actuator path format. Why it happens: Actuator's base path is configured as /api/v1 in this project (see management.endpoints.web.base-path), so health is at /api/v1/health. Springdoc paths may also vary depending on configuration. How to avoid: Ensure requestMatchers covers: /api/v1/health, /api/v1/api-docs/**, /api/v1/swagger-ui/**, /swagger-ui/**, /v3/api-docs/** (springdoc internal redirects). Warning signs: Health checks fail, Swagger UI returns 401.

Code Examples

JWT Creation with Nimbus JOSE+JWT (HMAC-SHA256)

// Source: https://connect2id.com/products/nimbus-jose-jwt/examples/jwt-with-hmac
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jwt.*;
import java.util.Date;

// Generate a random 256-bit secret at startup
byte[] secret = new byte[32];
new java.security.SecureRandom().nextBytes(secret);

// Create access token
JWTClaimsSet claims = new JWTClaimsSet.Builder()
    .subject(agentId)                              // sub = agentId
    .claim("group", group)                         // custom claim
    .claim("type", "access")                       // distinguish from refresh
    .issueTime(new Date())
    .expirationTime(new Date(System.currentTimeMillis() + 3600_000)) // 1 hour
    .build();

SignedJWT jwt = new SignedJWT(
    new JWSHeader(JWSAlgorithm.HS256),
    claims);
jwt.sign(new MACSigner(secret));
String tokenString = jwt.serialize();

// Validate token
SignedJWT parsed = SignedJWT.parse(tokenString);
boolean valid = parsed.verify(new MACVerifier(secret));
// Then check: claims.getExpirationTime().after(new Date())
// Then check: claims.getStringClaim("type").equals("access")

Ed25519 Keypair Generation and Signing (JDK 17)

// Source: https://howtodoinjava.com/java15/java-eddsa-example/
import java.security.*;
import java.util.Base64;
import java.nio.charset.StandardCharsets;

// Generate ephemeral keypair at startup
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("Ed25519");
KeyPair keyPair = keyGen.generateKeyPair();

// Export public key as Base64 (X.509 SubjectPublicKeyInfo DER)
String publicKeyBase64 = Base64.getEncoder().encodeToString(
    keyPair.getPublic().getEncoded());

// Sign a payload
Signature signer = Signature.getInstance("Ed25519");
signer.initSign(keyPair.getPrivate());
signer.update(payloadJson.getBytes(StandardCharsets.UTF_8));
byte[] sig = signer.sign();
String signatureBase64 = Base64.getEncoder().encodeToString(sig);

SecurityFilterChain Configuration

// Source: Spring Security 6.4 reference docs
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http,
                                            JwtService jwtService,
                                            AgentRegistryService registry) throws Exception {
        JwtAuthenticationFilter jwtFilter = new JwtAuthenticationFilter(jwtService, registry);

        http
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(
                    "/api/v1/health",
                    "/api/v1/agents/register",
                    "/api/v1/api-docs/**",
                    "/api/v1/swagger-ui/**",
                    "/swagger-ui/**",
                    "/v3/api-docs/**"
                ).permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

Bootstrap Token Validation with Constant-Time Comparison

import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;

public boolean validateBootstrapToken(String provided) {
    byte[] providedBytes = provided.getBytes(StandardCharsets.UTF_8);
    byte[] expectedBytes = bootstrapToken.getBytes(StandardCharsets.UTF_8);
    boolean match = MessageDigest.isEqual(providedBytes, expectedBytes);

    if (!match && previousBootstrapToken != null) {
        byte[] previousBytes = previousBootstrapToken.getBytes(StandardCharsets.UTF_8);
        match = MessageDigest.isEqual(providedBytes, previousBytes);
    }
    return match;
}

State of the Art

Old Approach Current Approach When Changed Impact
WebSecurityConfigurerAdapter SecurityFilterChain bean Spring Security 5.7 / Spring Boot 3.0 Must use lambda-style HttpSecurity configuration
antMatchers() requestMatchers() Spring Security 6.0 Method name changed; old code won't compile
Ed25519 via Bouncy Castle JDK built-in Ed25519 Java 15 (JEP 339) No external dependency needed for EdDSA
Session-based auth Stateless JWT Architectural pattern SessionCreationPolicy.STATELESS mandatory for REST APIs

Deprecated/outdated:

  • WebSecurityConfigurerAdapter: Removed in Spring Security 6.0. Use SecurityFilterChain bean instead.
  • antMatchers() / mvcMatchers(): Replaced by requestMatchers() in Spring Security 6.0.
  • authorizeRequests(): Replaced by authorizeHttpRequests() in Spring Security 6.0.

Open Questions

  1. Nimbus JOSE+JWT transitive availability

    • What we know: spring-boot-starter-security brings Spring Security 6.4.3. If spring-security-oauth2-jose is on the classpath, Nimbus is available transitively.
    • What's unclear: Whether the base spring-boot-starter-security (without OAuth2 resource server) includes Nimbus.
    • Recommendation: Add com.nimbusds:nimbus-jose-jwt explicitly as a dependency. This costs nothing if already transitive and ensures availability if not. Version 9.47 is current and compatible.
  2. Existing test adaptation scope

    • What we know: 21 existing integration tests use TestRestTemplate without any auth headers. All will fail when security is enabled.
    • What's unclear: Exact effort to adapt all tests.
    • Recommendation: Create a test utility class that generates valid test JWTs and bootstrap tokens. Set CAMELEER_AUTH_TOKEN=test-token in application-test.yml. Add JWT header to all test HTTP calls via a shared helper method.

Validation Architecture

Test Framework

Property Value
Framework JUnit 5 + Spring Boot Test (spring-boot-starter-test)
Config file cameleer-server-app/src/test/resources/application-test.yml
Quick run command mvn test -pl cameleer-server-app -Dtest=Security*Test -Dsurefire.reuseForks=false
Full suite command mvn clean verify

Phase Requirements to Test Map

Req ID Behavior Test Type Automated Command File Exists?
SECU-01 Protected endpoints reject requests without JWT; public endpoints accessible integration mvn test -pl cameleer-server-app -Dtest=SecurityFilterIT -Dsurefire.reuseForks=false No -- Wave 0
SECU-02 Refresh endpoint issues new access JWT from valid refresh token integration mvn test -pl cameleer-server-app -Dtest=JwtRefreshIT -Dsurefire.reuseForks=false No -- Wave 0
SECU-03 Ed25519 keypair generated at startup; public key in registration response integration mvn test -pl cameleer-server-app -Dtest=RegistrationSecurityIT -Dsurefire.reuseForks=false No -- Wave 0
SECU-04 SSE payloads carry valid Ed25519 signature integration mvn test -pl cameleer-server-app -Dtest=SseSigningIT -Dsurefire.reuseForks=false No -- Wave 0
SECU-05 Bootstrap token required for registration; rejects invalid/missing tokens integration mvn test -pl cameleer-server-app -Dtest=BootstrapTokenIT -Dsurefire.reuseForks=false No -- Wave 0
N/A JWT creation, validation, expiry logic unit mvn test -pl cameleer-server-app -Dtest=JwtServiceTest -Dsurefire.reuseForks=false No -- Wave 0
N/A Ed25519 signing and verification roundtrip unit mvn test -pl cameleer-server-app -Dtest=Ed25519SigningServiceTest -Dsurefire.reuseForks=false No -- Wave 0

Sampling Rate

  • Per task commit: mvn test -pl cameleer-server-app -Dsurefire.reuseForks=false
  • Per wave merge: mvn clean verify
  • Phase gate: Full suite green before /gsd:verify-work

Wave 0 Gaps

  • SecurityFilterIT.java -- covers SECU-01 (protected/public endpoint access)
  • JwtRefreshIT.java -- covers SECU-02 (refresh flow)
  • RegistrationSecurityIT.java -- covers SECU-03 + SECU-05 (bootstrap token + public key)
  • SseSigningIT.java -- covers SECU-04 (Ed25519 SSE signing)
  • BootstrapTokenIT.java -- covers SECU-05 (bootstrap token validation)
  • JwtServiceTest.java -- unit test for JWT creation/validation
  • Ed25519SigningServiceTest.java -- unit test for Ed25519 signing roundtrip
  • Update application-test.yml with CAMELEER_AUTH_TOKEN: test-token and security-related test config
  • Update ALL existing ITs to include JWT auth headers (21 test files affected)

Sources

Primary (HIGH confidence)

Secondary (MEDIUM confidence)

Tertiary (LOW confidence)

  • None -- all findings verified against primary sources

Metadata

Confidence breakdown:

  • Standard stack: HIGH - Spring Security 6.4.3 confirmed managed by Spring Boot 3.4.3, Nimbus well-documented, JDK Ed25519 verified for Java 17
  • Architecture: HIGH - SecurityFilterChain pattern is the documented standard for Spring Security 6.x, existing codebase has clear integration points
  • Pitfalls: HIGH - Double filter registration and test breakage are well-documented issues with Spring Security adoption; Ed25519 signing concerns are from domain knowledge

Research date: 2026-03-11 Valid until: 2026-04-11 (stable -- Spring Boot 3.4.x LTS, JDK 17 LTS)