--- name: identityserver-key-management description: Managing cryptographic signing keys in Duende IdentityServer, including automatic key management, KeyManagementOptions, data protection at rest, static key configuration, migration from static to automatic, and multi-instance deployment considerations. invocable: false --- # Key Management and Signing ## When to Use This Skill - Configuring automatic key management for signing token keys - Setting up static/manual signing keys from certificates or key vaults - Configuring key rotation intervals and key lifecycle - Migrating from static keys to automatic key management - Deploying IdentityServer in load-balanced or multi-instance environments - Protecting keys at rest using data protection - Configuring per-algorithm or per-resource signing - Troubleshooting key-related errors (CryptographicException, unprotecting key failures) Docs: https://docs.duendesoftware.com/identityserver/fundamentals/key-management/ ## Core Concepts IdentityServer issues cryptographically signed tokens: identity tokens, JWT access tokens, and logout tokens. These signatures require key material that can be managed automatically or manually (statically). ### Supported Signing Algorithms IdentityServer supports the `RS`, `PS`, and `ES` families: | Family | Algorithms | Key Type | | ------ | ------------------------- | -------- | | RS | `RS256`, `RS384`, `RS512` | RSA | | PS | `PS256`, `PS384`, `PS512` | RSA | | ES | `ES256`, `ES384`, `ES512` | ECDSA | ### Core Rotation Rule Regardless of approach, safe rotation obeys one rule: **publish a new public key in discovery (JWKS) BEFORE using it to sign tokens, and keep a RETIRED public key published until every token signed with it has expired.** Automatic Key Management enforces this overlap for you (Announced → Signing → Retired phases). With static/manual keys you must perform the overlap yourself via phased rotation (see [Manual Key Rotation](#manual-key-rotation-phased-approach)). ## Automatic Key Management (Recommended) Automatic Key Management handles key creation, rotation, announcement, and retirement. It is enabled by default and is part of the Business and Enterprise editions. ### Key Lifecycle Keys move through four phases: ``` Announced --> Signing --> Retired --> Deleted | | | | |<--Propagation-->| | | | |<--Rotation-->| | | | |<--Retention-->| ``` | Phase | Duration (default) | Purpose | | ------------- | -------------------------------------------- | --------------------------------------------- | | **Announced** | 14 days (`PropagationTime`) | Published in discovery, not yet signing | | **Signing** | 76 days (RotationInterval - PropagationTime) | Active signing credential | | **Retired** | 14 days (`RetentionDuration`) | In discovery for token validation only | | **Deleted** | After retention | Removed from discovery and optionally deleted | **Default schedule:** Keys rotate every 90 days, announced 14 days early, retained 14 days after rotation. ### Configuration ```csharp // Program.cs var idsvrBuilder = builder.Services.AddIdentityServer(options => { // Key rotates every 30 days options.KeyManagement.RotationInterval = TimeSpan.FromDays(30); // Announce new key 2 days in advance in discovery options.KeyManagement.PropagationTime = TimeSpan.FromDays(2); // Keep old key for 7 days in discovery for validation options.KeyManagement.RetentionDuration = TimeSpan.FromDays(7); // Don't delete keys after their retention period is over options.KeyManagement.DeleteRetiredKeys = false; }); ``` ### KeyManagement Options Reference | Property | Default | Description | | ------------------------------------ | --------- | --------------------------------------------------------- | | `Enabled` | `true` | Enable automatic key management | | `SigningAlgorithms` | `[RS256]` | Algorithms for which keys are managed | | `RsaKeySize` | `2048` | RSA key size in bits | | `RotationInterval` | 90 days | Age at which keys stop signing | | `PropagationTime` | 14 days | Time for new keys to propagate to all servers and clients | | `RetentionDuration` | 14 days | Duration retired keys remain in discovery | | `DeleteRetiredKeys` | `true` | Delete keys after retention period | | `KeyPath` | `{ContentRootPath}/keys` | File system path for default key store | | `DataProtectKeys` | `true` | Encrypt keys at rest using data protection | | `KeyCacheDuration` | 24 hours | Cache duration for keys from store | | `InitializationDuration` | 5 minutes | Synchronization window on first key creation | | `InitializationSynchronizationDelay` | 5 seconds | Delay between retries during initialization | ### Multiple Signing Algorithms Configure multiple algorithms to serve clients with different requirements. The first algorithm in the list is the default for signing tokens. ```csharp options.KeyManagement.SigningAlgorithms = new[] { // RS256 for older clients (with X.509 wrapping) new SigningAlgorithmOptions(SecurityAlgorithms.RsaSha256) { UseX509Certificate = true }, // PS256 new SigningAlgorithmOptions(SecurityAlgorithms.RsaSsaPssSha256), // ES256 new SigningAlgorithmOptions(SecurityAlgorithms.EcdsaSha256) }; ``` Override the default on a per-client or per-resource basis: ```csharp // Client level var client = new Client { AllowedIdentityTokenSigningAlgorithms = { SecurityAlgorithms.RsaSsaPssSha256 } }; // API Resource level var api = new ApiResource("invoice") { AllowedAccessTokenSigningAlgorithms = { SecurityAlgorithms.RsaSsaPssSha256 } }; ``` ## Key Storage ### Default: File System The default `FileSystemKeyStore` writes keys to the `KeyPath` directory (defaults to `{ContentRootPath}/keys`). This directory must be: - Excluded from source control - Accessible (read/write) to all load-balanced instances if using file-based storage ```csharp // Program.cs var idsvrBuilder = builder.Services.AddIdentityServer(options => { options.KeyManagement.KeyPath = "/home/shared/keys"; }); ``` ### EntityFramework Store Use the EF operational store for database-backed key storage: ```csharp // Program.cs builder.Services.AddIdentityServer() .AddOperationalStore(options => { options.ConfigureDbContext = b => b.UseSqlServer(connectionString); }); ``` ### Custom Store Implement `ISigningKeyStore` for custom storage (e.g., Azure Key Vault, AWS KMS): ```csharp // Program.cs builder.Services.AddIdentityServer() .AddSigningKeyStore(); ``` The store interface methods: - `LoadKeysAsync` - load all keys (cached for `KeyCacheDuration`) - `StoreKeyAsync` - persist a new key - `DeleteKeyAsync` - remove a retired key ## Encryption of Keys at Rest By default, keys are protected at rest using ASP.NET Core Data Protection (`DataProtectKeys = true`). Keep this enabled unless your custom `ISigningKeyStore` already ensures encryption (e.g., Azure Key Vault). ```csharp // ❌ WRONG: Disabling without alternative encryption options.KeyManagement.DataProtectKeys = false; // ✅ CORRECT: Only disable when using a vault that encrypts at rest options.KeyManagement.DataProtectKeys = false; // OK if using Azure Key Vault via custom ISigningKeyStore ``` ### Data Protection Configuration for Production Data protection must be properly configured for key encryption to work across instances. See [ASP.NET Core Data Protection](https://docs.duendesoftware.com/general/data-protection/) for foundational concepts and troubleshooting. ```csharp // Program.cs builder.Services.AddDataProtection() .PersistKeysToDbContext() // or PersistKeysToAzureBlobStorage, etc. .ProtectKeysWithCertificate(certificate) // or ProtectKeysWithAzureKeyVault .SetApplicationName("My.IdentityServer"); ``` ### Common Data Protection Problems | Symptom | Cause | Fix | | -------------------------------------------------------------------- | ------------------------------------------------- | ---------------------------------------- | | `CryptographicException: The key {ID} was not found in the key ring` | Data protection keys not shared across instances | Configure shared key persistence | | `Error unprotecting key with kid {ID}` | Keys protected by a different data protection key | Ensure consistent data protection config | | Keys work locally but fail in deployment | Default file-based storage uses ephemeral storage | Use durable, shared storage | | Keys break after redeployment | Application name changed or not set | Set explicit `SetApplicationName()` | ## Static Key Management For scenarios where you want explicit control over signing keys or your license does not include automatic key management. ### Disabling Automatic Key Management ```csharp // Program.cs var idsvrBuilder = builder.Services.AddIdentityServer(options => { options.KeyManagement.Enabled = false; }); ``` ### Adding Static Signing Keys ```csharp // Program.cs var idsvrBuilder = builder.Services.AddIdentityServer(); var key = LoadKeyFromVault(); // your code to load the key idsvrBuilder.AddSigningCredential(key, SecurityAlgorithms.RsaSha256); ``` Multiple signing keys can be registered. The first one added is the default. ### Adding Validation Keys Register public keys that should be accepted for token validation (used during key rotation): ```csharp idsvrBuilder.AddValidationKey(oldKey, SecurityAlgorithms.RsaSha256); ``` ### Creating Self-Signed Certificates ```csharp var name = "MySelfSignedCertificate"; using var rsa = RSA.Create(keySizeInBits: 2048); var request = new CertificateRequest( subjectName: $"CN={name}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1 ); var certificate = request.CreateSelfSigned( DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1) ); var pfxBytes = certificate.Export(X509ContentType.Pfx, password: "password"); File.WriteAllBytes($"{name}.pfx", pfxBytes); ``` ### Loading Keys from Disk or Certificate Store ```csharp // From PFX file var bytes = File.ReadAllBytes("mycertificate.pfx"); var certificate = X509CertificateLoader.LoadPkcs12(bytes, "password"); // From certificate store var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); store.Open(OpenFlags.ReadWrite); var certificate = store.Certificates.First(c => c.Thumbprint == ""); ``` ## Manual Key Rotation (Phased Approach) When using static keys, rotation must be performed carefully to avoid validation failures. ### Why Phased Rotation is Necessary 1. **Client/API caching** - Clients and APIs cache keys (default: 24 hours). Using a new key immediately means cached clients cannot validate tokens signed with it. 2. **Existing tokens** - Tokens signed with the old key are still valid. Removing the old key immediately invalidates those tokens. ### Phase 1: Announce the New Key Sign with the old key, publish the new key as a validation key: ```csharp var idsvrBuilder = builder.Services.AddIdentityServer(options => { options.KeyManagement.Enabled = false; }); var oldKey = LoadOldKeyFromVault(); var newKey = LoadNewKeyFromVault(); idsvrBuilder.AddSigningCredential(oldKey, SecurityAlgorithms.RsaSha256); idsvrBuilder.AddValidationKey(newKey, SecurityAlgorithms.RsaSha256); ``` **Wait:** Until all clients/APIs have refreshed their caches (default 24 hours). ### Phase 2: Start Signing with the New Key Swap signing and validation keys: ```csharp var idsvrBuilder = builder.Services.AddIdentityServer(options => { options.KeyManagement.Enabled = false; }); var oldKey = LoadOldKeyFromVault(); var newKey = LoadNewKeyFromVault(); idsvrBuilder.AddSigningCredential(newKey, SecurityAlgorithms.RsaSha256); idsvrBuilder.AddValidationKey(oldKey, SecurityAlgorithms.RsaSha256); ``` **Wait:** Until all tokens signed with the old key have expired (default access token lifetime: 1 hour). ### Phase 3: Remove the Old Key ```csharp var idsvrBuilder = builder.Services.AddIdentityServer(options => { options.KeyManagement.Enabled = false; }); var newKey = LoadNewKeyFromVault(); idsvrBuilder.AddSigningCredential(newKey, SecurityAlgorithms.RsaSha256); ``` ## Migrating from Static to Automatic Key Management This is also a three-phase process where automatic keys gradually replace static keys. ### Phase 1: Enable Automatic Key Management, Keep Signing with Static Key The static signing credential takes precedence over automatic keys. Automatic key management begins creating and announcing keys in discovery. ```csharp var idsvrBuilder = builder.Services.AddIdentityServer(options => { options.KeyManagement.Enabled = true; }); var oldKey = LoadOldKeyFromVault(); idsvrBuilder.AddSigningCredential(oldKey, SecurityAlgorithms.RsaSha256); ``` **Wait:** Until all APIs and clients have updated their caches with the new automatic keys. ### Phase 2: Switch to Automatic Signing, Keep Static for Validation Remove the static signing credential; keep it as a validation key: ```csharp var idsvrBuilder = builder.Services.AddIdentityServer(options => { options.KeyManagement.Enabled = true; }); var oldKey = LoadOldKeyFromVault(); idsvrBuilder.AddValidationKey(oldKey, SecurityAlgorithms.RsaSha256); ``` **Wait:** Until all tokens signed with the old static key have expired. ### Phase 3: Remove Static Key Entirely ```csharp var idsvrBuilder = builder.Services.AddIdentityServer(options => { options.KeyManagement.Enabled = true; }); ``` ## Multi-Instance / Load-Balanced Deployment ### Requirements | Concern | Solution | | ----------------------------------- | --------------------------------------------------- | | Key storage shared across instances | Use EF operational store or shared file system | | Data protection keys shared | Configure shared data protection key persistence | | Key cache synchronization | `PropagationTime` handles cache refresh windows | | Initialization race condition | `InitializationDuration` (5 min) allows server sync | ### File System Store in Load-Balanced Environments All instances need read/write access to the `KeyPath`: ```csharp options.KeyManagement.KeyPath = "/shared-volume/identity-keys"; ``` ### Recommended: Database-Backed Store ```csharp builder.Services.AddIdentityServer() .AddOperationalStore(options => { options.ConfigureDbContext = b => b.UseSqlServer(connectionString); }); ``` ## OIDC + SAML Shared Signing Keys When the SAML component is enabled, IdentityServer uses the **same signing credentials** for OIDC tokens and SAML messages — one key store, one rotation schedule. Rotated public keys are published in parallel via the OIDC JWKS endpoint and the SAML IdP metadata during rollover. ### X.509 Requirement (SAML metadata needs certificates) SAML metadata requires X.509 certificates, not raw keys: - **Automatic Key Management** — creates RSA keys by default, and the SAML component **auto-wraps** managed RSA keys in self-signed X.509 certificates. You do **not** need to set `UseX509Certificate` just to enable SAML. - **Static Key Management** — you **must** configure an X.509 signing certificate **with a private key**. A manually registered raw RSA key — including one from `AddDeveloperSigningCredential()` — **cannot** be auto-wrapped for SAML. ### Constraints - The default SAML signing service supports **RSA only**. `UseX509Certificate` is **not** supported for EC (`ES`) keys. - For a different certificate, independent rotation, or an external key system, implement a custom `ISamlSigningService`. ### Rotation Knobs (shared with OIDC) | Knob | Effect for SAML | | ------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `PropagationTime` | How long a new managed key is published before it starts signing — set long enough for all SPs to refresh IdP metadata. | | `RetentionDuration` | Keeps the previous certificate in metadata while SPs may still validate old messages (and old OIDC tokens remain valid). | Service providers with **statically configured** IdP certificates must update those certs on every rotation. ## Common Pitfalls 1. **`keys` directory in source control** - Contains cryptographic secrets. Add the `keys` directory (under the app content root) to `.gitignore`. If accidentally committed, the keys may be data-protected with development-only data protection keys and fail in production. 2. **Data protection not configured for production** - Default data protection uses machine-specific keys. In containers or multi-instance deployments, keys protected by one instance cannot be read by another. Always configure shared, persistent data protection. 3. **Immediate key rotation** - Switching signing keys without a transition period causes validation failures. Use the phased approach or rely on automatic key management. 4. **Disabling `DataProtectKeys` without alternative** - Turning off key encryption without ensuring your store encrypts at rest exposes signing keys to anyone with storage access. 5. **X.509 certificate expiration confusion** - IdentityServer does not validate X.509 certificate expiration dates. Expired certificates still work for signing. The expiration date is a policy decision, not a technical enforcement. 6. **Not setting `PropagationTime` long enough** - If clients/APIs cache keys longer than your propagation time, new keys may not be in their caches when signing starts. Ensure `PropagationTime` exceeds your longest cache duration. 7. **Mixing up Data Protection keys and signing keys** - These are completely separate. Data Protection uses symmetric encryption for sensitive data at rest. Signing keys use asymmetric cryptography for token signatures. Both must be properly configured.