Policy Enforcement Guide
Target Audience: Security Engineers, Platform Administrators
Version: 3.0.0 Last Updated: December 2025
Overview
This guide explains how ANKASecure enforces cryptographic policies at runtime, with focus on hybrid key requirements and regulatory compliance (BSI, ANSSI, ETSI, NIST).
Policy Enforcement Architecture
Components
graph TD
subgraph Admin-API
A["Admin configures policy"] --> B["PostgreSQL:<br/>policy tables"]
B --> C["PolicySyncService"]
C --> D["Redis:<br/>policy cache"]
end
subgraph Core-API
E["User request"] --> F["KeyGenerationService"]
F --> G["AlgorithmAvailabilityPolicyHolder"]
G --> H["5-Level Cache"]
H --> I{"Cache Hit?"}
I -->|L1| J["Caffeine<br/>in-memory"]
I -->|L2| K["Redis"]
I -->|L3| L["Local disk"]
I -->|L4| M["Fail-closed<br/>deny all"]
J & K & L & M --> N["AlgorithmAvailabilityPolicy"]
N --> O["Evaluate request"]
O --> P{"Permitted?"}
P -->|Yes| Q["Generate key"]
P -->|No| R["Throw AlgorithmNotAdmittedException (400 algorithm-not-admitted)"]
end
D -.sync.-> K
style Q fill:#ccffcc
style R fill:#ffcccc Cache Hierarchy
ANKASecure uses a 5-level cache for policy resolution:
| Level | Storage | TTL | Purpose |
|---|---|---|---|
| L1 | Caffeine (in-memory) | 300s | Ultra-fast lookups (<1ms) |
| L2 | Redis | 3600s | Cross-instance consistency |
| L3 | Local disk | 24h | Redis unavailability fallback |
| L4 | Fail-closed | N/A | Emergency mode (deny all) |
| L5 | Decision cache | Per-tenant | Caches permit/deny decisions |
Performance: ~0.5ms average policy resolution time (L1 cache hit rate >95%)
Enforcement Flow for Single Keys
Step-by-Step Validation
sequenceDiagram
participant User
participant API as KeyGenerationService
participant Policy as PolicyHolder
participant Cache as 5-Level Cache
participant Validator as AlgorithmValidator
User->>API: POST /api/v3/admin/tenants/{tenantId}/keys<br/>{kty: ML-KEM, algorithm: ML-KEM-768}
API->>Policy: Get policy for tenantId
Policy->>Cache: Resolve policy
Cache-->>Policy: AlgorithmAvailabilityPolicy
Note over Policy: Policy loaded:<br/>BSI_HYBRID_ENFORCED<br/>requireComposite=true<br/>minSecurityLevel=3
API->>Validator: Check algorithm allowed
Validator-->>API: ✅ ML-KEM-768 in allowedFamilies
API->>Validator: Check security level
Validator-->>API: ✅ Level 3 ≥ minimum 3
API->>Validator: Check hybrid requirement
Note over Validator: ML-KEM is PQC?<br/>Yes ✅<br/>Hash-based signature?<br/>No ❌<br/>→ Hybrid REQUIRED
Validator-->>API: ❌ DENY - Hybrid required
API->>User: HTTP 400 algorithm-not-admitted, refusedBy requireComposite<br/>"... not admitted by the tenant algorithm policy (template ...);<br/>the refusing policy field is requireComposite ..." Validation Logic
// Pseudocode for clarity
boolean validateKeyGeneration(Request request, Policy policy) {
// Every refusal below is worded by AlgorithmAdmissionMessages: the algorithm, the policy that
// judged it (the tenant's own, or the deployment policy it inherits, with its template) and the
// refusing policy field. Each refusal is a 400 algorithm-not-admitted carrying the catalogue
// algorithm and the refusing check (extensions.algorithm, extensions.refusedBy).
// Step 1: Algorithm admitted? (explicit list, operations, category, family, level, standards)
AlgorithmAdmission admission = admitSimple(request.alg, request.keyOps);
if (!admission.admitted()) {
throw new AlgorithmNotAdmittedException(
detail(request.alg, admission.dimension(), admission.origin()),
request.alg, refusedBy(admission.dimension()));
}
// Step 2: Security level ≥ minimum?
int algorithmLevel = getSecurityLevel(request.alg);
if (algorithmLevel < policy.minSecurityLevel) {
throw new AlgorithmNotAdmittedException(
detail(request.alg, MIN_SECURITY_LEVEL, policy.origin), request.alg, "minSecurityLevel");
}
// Step 3: Hybrid required? Post-quantum is the catalogue category POST_QUANTUM;
// hash-based is SLH-DSA, XMSS (with XMSS^MT) and LMS (with HSS).
if (policy.requireComposite && isPostQuantum(request.kty)) {
if (!isHashBasedSignature(request.kty)) {
// Worded by AlgorithmAdmissionMessages.requiresComposite: the algorithm, the policy in
// force (tenant or inherited deployment, with its template) and the field requireComposite.
throw new AlgorithmNotAdmittedException(
requiresComposite(request.alg, policy.origin), request.alg, "requireComposite");
}
}
return true; // All validations passed
}
Enforcement Flow for Composite Keys
Step-by-Step Validation
sequenceDiagram
participant User
participant API as KeyGenerationService
participant Policy as PolicyHolder
participant Validator as CompositeValidator
User->>API: POST /api/v3/key-management/keys<br/>{type: COMPOSITE, alg: X448+ML-KEM-768, kdf: CatKDF}
API->>Validator: Check algorithm compatibility
Note over Validator: X448 + ML-KEM-768<br/>Both Level 3<br/>Within 2 steps ✅
Validator-->>API: ✅ Compatible
API->>Policy: Get policy for tenantId
Policy-->>API: ETSI_HYBRID_ENFORCED
API->>Validator: CompositeAdmissionRule
Note over Validator: families OKP, ML-KEM declared ✅<br/>weakest-link L3 in allowedLevels, ≥ 3 ✅<br/>pair standards include ETSI ✅<br/>mode COMPOSITE_KEM_COMBINE ✅<br/>kdf CatKDF in allowedKdfs ✅
Validator-->>API: ✅ Admitted
API->>User: HTTP 201 Created Every policy dimension is enforced when a composite key is created
A composite create, import or rotation target, and the composite material of a SIMPLE → COMPOSITE transition, is judged by CompositeAdmissionRule against the tenant's effective policy, and the first failing dimension refuses the request with HTTP 400 application/problem+json (type algorithm-not-admitted) whose detail names the algorithm and that dimension and whose extensions.refusedBy names it as data (a fail-closed refusal names no dimension in detail and carries failClosed). An unknown composite is invalid-input with the invalid-composite-algorithm detail. allowedCompositeModes and allowedKdfs are part of that judgement, so they are refused per request, not only when a policy is saved.
allowedKdfs is judged last, and on composite KEM pairs only: the KDF the request names — or the pair's catalogue default, HKDF-SHA256, when it names none — must equal a member of the list exactly (no trim, no case folding). A template that names only HKDF-SHA256 therefore refuses a request naming HKDF-SHA384, HKDF-SHA512, CatKDF or CasKDF. A policy's allowedKdfs accepts only the five published tokens HKDF-SHA256, HKDF-SHA384, HKDF-SHA512, CatKDF, CasKDF; any other value, "" included, is refused with 400 when the policy is saved.
The Admin API also bounds a tenant policy by its deployment ceiling when either is saved (per dimension, with composite explicitAlgorithms entries judged by the same rule); see the declared residual in Policy Management. The evaluation order is in Algorithm-Availability Policy — composite (hybrid) admission.
Real-World Scenarios
Scenario 1: German Federal Agency
Configuration:
PUT /api/v3/admin/tenants/{tenantId}/policies/algorithm-availability
{"templateName": "BSI_HYBRID_ENFORCED"}
User Operations:
| Request | Result | Reason |
|---|---|---|
| Generate ML-KEM-768 standalone | ❌ DENIED | BSI requires hybrid |
| Generate ML-KEM-512 standalone | ❌ DENIED | Level 1 < Level 3 minimum |
| Generate SLH-DSA standalone | ✅ ALLOWED | Hash-based exception |
Generate brainpoolP384r1+ML-KEM-1024, HKDF-SHA256 | ✅ ALLOWED | Admitted on every dimension (both components BSI-recommended, BSI in its attribution) |
Generate brainpoolP384r1+ML-KEM-1024, CatKDF | ✅ ALLOWED | CatKDF is in allowedKdfs |
Generate brainpoolP384r1+ML-KEM-1024, CasKDF | ❌ DENIED | Not in allowedKdfs (HKDF-SHA256, CatKDF): BSI TR-02102-1 Table 2.1 does not recommend the cascade combiner |
Generate brainpoolP384r1+ML-KEM-1024, HKDF-SHA384 | ❌ DENIED | Not in allowedKdfs (HKDF-SHA256, CatKDF), judged after every other dimension admitted the pair |
Generate P-256+ML-KEM-768 | ❌ DENIED | Level 1 not in allowedLevels (L3, L5), checked before minSecurityLevel |
| Generate any RSA composite | ❌ DENIED | No RSA pairing exists in CompositeAlgorithmCatalogue; the request fails at algorithm resolution, before any policy check |
Scenario 2: French Defense Contractor
Configuration:
PUT /api/v3/admin/tenants/{tenantId}/policies/algorithm-availability
{"templateName": "ANSSI_HYBRID_ENFORCED"}
User Operations:
| Request | Result | Reason |
|---|---|---|
| Generate ML-DSA-65 standalone | ❌ DENIED | ANSSI requires hybrid |
Generate P-384+ML-DSA-65 | ✅ ALLOWED | Admitted; a signature pair has no KDF, so allowedKdfs is not consulted |
Generate Ed448+ML-DSA-65 | ❌ DENIED | OKP is not a declared family (no OKP algorithm or pair carries ANSSI), refused at allowedFamilies |
Generate P-384+ML-KEM-768, CatKDF | ✅ ALLOWED | CatKDF is in allowedKdfs |
Generate P-384+ML-KEM-768, CasKDF | ✅ ALLOWED | CasKDF is in allowedKdfs |
Generate P-384+ML-KEM-768, HKDF-SHA512 | ❌ DENIED | Not in allowedKdfs: ANSSI lists HKDF-SHA256, CatKDF and CasKDF |
| Generate any FALCON composite | ❌ DENIED | No FALCON pairing exists in CompositeAlgorithmCatalogue; the request fails at algorithm resolution, before any policy check |
Key Difference: ANSSI admits CasKDF, which the BSI template refuses.
Scenario 3: EU Telecommunications Operator
Configuration:
PUT /api/v3/admin/tenants/{tenantId}/policies/algorithm-availability
{"templateName": "ETSI_HYBRID_ENFORCED"}
User Operations:
| Request | Result | Reason |
|---|---|---|
| Generate ML-KEM-1024 standalone | ❌ DENIED | ETSI requires hybrid |
Generate X448+ML-KEM-768, CatKDF | ✅ ALLOWED | Admitted; CatKDF is in allowedKdfs |
Generate X448+ML-KEM-768, CasKDF | ✅ ALLOWED | Admitted; CasKDF is in allowedKdfs |
Generate COMPOSITE_SIGNATURE (Ed448+ML-DSA-65) | ❌ DENIED | ML-DSA is not a declared family, refused at allowedFamilies before allowedCompositeModes (which also names only COMPOSITE_KEM_COMBINE) is reached |
Key Difference: ETSI focuses on KEM for key exchange (blocks signature modes).
Policy Synchronization
Update Propagation
sequenceDiagram
participant Admin
participant AdminAPI as Admin-API
participant DB as PostgreSQL
participant Sync as PolicySyncService
participant Redis
participant CoreAPI as Core-API Instances
Admin->>AdminAPI: PUT /api/v3/admin/tenants/{id}/policies/algorithm-availability
AdminAPI->>DB: Save policy
DB-->>AdminAPI: Saved
AdminAPI->>Sync: Trigger sync
Sync->>Redis: Publish policy update
Redis-->>Sync: Published
Sync->>Redis: SET ankasecure:policy:algorithm:{tenantId}
Redis-->>Sync: Stored (TTL 3600s)
Redis->>CoreAPI: Pub/Sub notification
CoreAPI->>CoreAPI: Invalidate Caffeine cache for tenant
Note over CoreAPI: Next request will fetch<br/>updated policy from Redis Propagation Time: ~1-2 seconds
Zero Downtime: Policy updates do not require service restart.
Troubleshooting
Common Errors
Error: "Algorithm ML-KEM-768 is not admitted by the tenant algorithm policy (template ...); the refusing policy field is requireComposite ..."
Cause: Tenant policy has requireComposite=true, user attempted standalone PQC key generation.
Solution:
# Instead of:
POST /api/v3/admin/tenants/{tenantId}/keys
{"kid": "k1", "kty": "ML-KEM", "algorithm": "ML-KEM-768"}
# Use:
POST /api/v3/admin/tenants/{tenantId}/keys
{
"kid": "k1",
"kty": "COMPOSITE",
"algorithm": "brainpoolP384r1+ML-KEM-1024"
}
Error: "Algorithm (Level 1) below minimum security level 3"
Cause: Tenant policy enforces minimum Level 3, user requested Level 1 algorithm.
Solution: Use Level 3 or higher algorithms:
| Level 1 (Blocked) | Level 3 (Allowed) | Level 5 (Allowed) |
|---|---|---|
| ML-KEM-512 | ML-KEM-768 | ML-KEM-1024 |
| ML-DSA-44 | ML-DSA-65 | ML-DSA-87 |
| RSA-2048 | RSA-3072 | RSA-4096 |
Error: "Algorithm P-384+ML-KEM-768 is not admitted by the tenant algorithm policy (template ANSSI_HYBRID_ENFORCED); the refusing policy field is allowedKdfs."
Cause: the request named a KDF the tenant's allowedKdfs does not list; the ANSSI template lists HKDF-SHA256, CatKDF and CasKDF.
Solution: Change KDF in request:
How to Check Active Policy
Query tenant's current policy:
GET /api/v3/admin/tenants/{tenantId}/policies/algorithm-availability
# Response:
{
"templateName": "BSI_HYBRID_ENFORCED",
"requireComposite": true,
"allowedKdfs": ["HKDF-SHA256", "CatKDF"],
"minSecurityLevel": 3,
"regulatoryFramework": "BSI-TR-02102-1"
}
See Also
- Policy Templates*
- Regulatory Compliance Matrix
- Composite Keys Overview*
- Template Selection Guide