← Duende SkillsCONTENT HISTORY

Update to Duende Skills

Snapshot Sep 30, 2026 · 23:14 UTC · version 0.3.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "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.",
  "included_files": [],
  "name": "identityserver-key-management",
  "skill_md_contents": "---\nname: identityserver-key-management\ndescription: 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.\ninvocable: false\n---\n\n# Key Management and Signing\n\n## When to Use This Skill\n\n- Configuring automatic key management for signing token keys\n- Setting up static/manual signing keys from certificates or key vaults\n- Configuring key rotation intervals and key lifecycle\n- Migrating from static keys to automatic key management\n- Deploying IdentityServer in load-balanced or multi-instance environments\n- Protecting keys at rest using data protection\n- Configuring per-algorithm or per-resource signing\n- Troubleshooting key-related errors (CryptographicException, unprotecting key failures)\n\nDocs: https://docs.duendesoftware.com/identityserver/fundamentals/key-management/\n\n## Core Concepts\n\nIdentityServer 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).\n\n### Supported Signing Algorithms\n\nIdentityServer supports the `RS`, `PS`, and `ES` families:\n\n| Family | Algorithms                | Key Type |\n| ------ | ------------------------- | -------- |\n| RS     | `RS256`, `RS384`, `RS512` | RSA      |\n| PS     | `PS256`, `PS384`, `PS512` | RSA      |\n| ES     | `ES256`, `ES384`, `ES512` | ECDSA    |\n\n### Core Rotation Rule\n\nRegardless 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)).\n\n## Automatic Key Management (Recommended)\n\nAutomatic Key Management handles key creation, rotation, announcement, and retirement. It is enabled by default and is part of the Business and Enterprise editions.\n\n### Key Lifecycle\n\nKeys move through four phases:\n\n```\nAnnounced --> Signing --> Retired --> Deleted\n   |             |            |          |\n   |<--Propagation-->|        |          |\n   |             |<--Rotation-->|        |\n   |             |            |<--Retention-->|\n```\n\n| Phase         | Duration (default)                           | Purpose                                       |\n| ------------- | -------------------------------------------- | --------------------------------------------- |\n| **Announced** | 14 days (`PropagationTime`)                  | Published in discovery, not yet signing       |\n| **Signing**   | 76 days (RotationInterval - PropagationTime) | Active signing credential                     |\n| **Retired**   | 14 days (`RetentionDuration`)                | In discovery for token validation only        |\n| **Deleted**   | After retention                              | Removed from discovery and optionally deleted |\n\n**Default schedule:** Keys rotate every 90 days, announced 14 days early, retained 14 days after rotation.\n\n### Configuration\n\n```csharp\n// Program.cs\nvar idsvrBuilder = builder.Services.AddIdentityServer(options =>\n{\n    // Key rotates every 30 days\n    options.KeyManagement.RotationInterval = TimeSpan.FromDays(30);\n\n    // Announce new key 2 days in advance in discovery\n    options.KeyManagement.PropagationTime = TimeSpan.FromDays(2);\n\n    // Keep old key for 7 days in discovery for validation\n    options.KeyManagement.RetentionDuration = TimeSpan.FromDays(7);\n\n    // Don't delete keys after their retention period is over\n    options.KeyManagement.DeleteRetiredKeys = false;\n});\n```\n\n### KeyManagement Options Reference\n\n| Property                             | Default   | Description                                               |\n| ------------------------------------ | --------- | --------------------------------------------------------- |\n| `Enabled`                            | `true`    | Enable automatic key management                           |\n| `SigningAlgorithms`                  | `[RS256]` | Algorithms for which keys are managed                     |\n| `RsaKeySize`                         | `2048`    | RSA key size in bits                                      |\n| `RotationInterval`                   | 90 days   | Age at which keys stop signing                            |\n| `PropagationTime`                    | 14 days   | Time for new keys to propagate to all servers and clients |\n| `RetentionDuration`                  | 14 days   | Duration retired keys remain in discovery                 |\n| `DeleteRetiredKeys`                  | `true`    | Delete keys after retention period                        |\n| `KeyPath`                            | `{ContentRootPath}/keys` | File system path for default key store               |\n| `DataProtectKeys`                    | `true`    | Encrypt keys at rest using data protection                |\n| `KeyCacheDuration`                   | 24 hours  | Cache duration for keys from store                        |\n| `InitializationDuration`             | 5 minutes | Synchronization window on first key creation              |\n| `InitializationSynchronizationDelay` | 5 seconds | Delay between retries during initialization               |\n\n### Multiple Signing Algorithms\n\nConfigure multiple algorithms to serve clients with different requirements. The first algorithm in the list is the default for signing tokens.\n\n```csharp\noptions.KeyManagement.SigningAlgorithms = new[]\n{\n    // RS256 for older clients (with X.509 wrapping)\n    new SigningAlgorithmOptions(SecurityAlgorithms.RsaSha256) { UseX509Certificate = true },\n\n    // PS256\n    new SigningAlgorithmOptions(SecurityAlgorithms.RsaSsaPssSha256),\n\n    // ES256\n    new SigningAlgorithmOptions(SecurityAlgorithms.EcdsaSha256)\n};\n```\n\nOverride the default on a per-client or per-resource basis:\n\n```csharp\n// Client level\nvar client = new Client\n{\n    AllowedIdentityTokenSigningAlgorithms = { SecurityAlgorithms.RsaSsaPssSha256 }\n};\n\n// API Resource level\nvar api = new ApiResource(\"invoice\")\n{\n    AllowedAccessTokenSigningAlgorithms = { SecurityAlgorithms.RsaSsaPssSha256 }\n};\n```\n\n## Key Storage\n\n### Default: File System\n\nThe default `FileSystemKeyStore` writes keys to the `KeyPath` directory (defaults to `{ContentRootPath}/keys`). This directory must be:\n\n- Excluded from source control\n- Accessible (read/write) to all load-balanced instances if using file-based storage\n\n```csharp\n// Program.cs\nvar idsvrBuilder = builder.Services.AddIdentityServer(options =>\n{\n    options.KeyManagement.KeyPath = \"/home/shared/keys\";\n});\n```\n\n### EntityFramework Store\n\nUse the EF operational store for database-backed key storage:\n\n```csharp\n// Program.cs\nbuilder.Services.AddIdentityServer()\n    .AddOperationalStore(options =>\n    {\n        options.ConfigureDbContext = b =>\n            b.UseSqlServer(connectionString);\n    });\n```\n\n### Custom Store\n\nImplement `ISigningKeyStore` for custom storage (e.g., Azure Key Vault, AWS KMS):\n\n```csharp\n// Program.cs\nbuilder.Services.AddIdentityServer()\n    .AddSigningKeyStore<YourCustomStore>();\n```\n\nThe store interface methods:\n\n- `LoadKeysAsync` - load all keys (cached for `KeyCacheDuration`)\n- `StoreKeyAsync` - persist a new key\n- `DeleteKeyAsync` - remove a retired key\n\n## Encryption of Keys at Rest\n\nBy 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).\n\n```csharp\n// ❌ WRONG: Disabling without alternative encryption\noptions.KeyManagement.DataProtectKeys = false;\n\n// ✅ CORRECT: Only disable when using a vault that encrypts at rest\noptions.KeyManagement.DataProtectKeys = false; // OK if using Azure Key Vault via custom ISigningKeyStore\n```\n\n### Data Protection Configuration for Production\n\nData 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.\n\n```csharp\n// Program.cs\nbuilder.Services.AddDataProtection()\n    .PersistKeysToDbContext<MyDbContext>()        // or PersistKeysToAzureBlobStorage, etc.\n    .ProtectKeysWithCertificate(certificate)      // or ProtectKeysWithAzureKeyVault\n    .SetApplicationName(\"My.IdentityServer\");\n```\n\n### Common Data Protection Problems\n\n| Symptom                                                              | Cause                                             | Fix                                      |\n| -------------------------------------------------------------------- | ------------------------------------------------- | ---------------------------------------- |\n| `CryptographicException: The key {ID} was not found in the key ring` | Data protection keys not shared across instances  | Configure shared key persistence         |\n| `Error unprotecting key with kid {ID}`                               | Keys protected by a different data protection key | Ensure consistent data protection config |\n| Keys work locally but fail in deployment                             | Default file-based storage uses ephemeral storage | Use durable, shared storage              |\n| Keys break after redeployment                                        | Application name changed or not set               | Set explicit `SetApplicationName()`      |\n\n## Static Key Management\n\nFor scenarios where you want explicit control over signing keys or your license does not include automatic key management.\n\n### Disabling Automatic Key Management\n\n```csharp\n// Program.cs\nvar idsvrBuilder = builder.Services.AddIdentityServer(options =>\n{\n    options.KeyManagement.Enabled = false;\n});\n```\n\n### Adding Static Signing Keys\n\n```csharp\n// Program.cs\nvar idsvrBuilder = builder.Services.AddIdentityServer();\nvar key = LoadKeyFromVault(); // your code to load the key\nidsvrBuilder.AddSigningCredential(key, SecurityAlgorithms.RsaSha256);\n```\n\nMultiple signing keys can be registered. The first one added is the default.\n\n### Adding Validation Keys\n\nRegister public keys that should be accepted for token validation (used during key rotation):\n\n```csharp\nidsvrBuilder.AddValidationKey(oldKey, SecurityAlgorithms.RsaSha256);\n```\n\n### Creating Self-Signed Certificates\n\n```csharp\nvar name = \"MySelfSignedCertificate\";\n\nusing var rsa = RSA.Create(keySizeInBits: 2048);\n\nvar request = new CertificateRequest(\n    subjectName: $\"CN={name}\",\n    rsa,\n    HashAlgorithmName.SHA256,\n    RSASignaturePadding.Pkcs1\n);\n\nvar certificate = request.CreateSelfSigned(\n    DateTimeOffset.Now,\n    DateTimeOffset.Now.AddYears(1)\n);\n\nvar pfxBytes = certificate.Export(X509ContentType.Pfx, password: \"password\");\nFile.WriteAllBytes($\"{name}.pfx\", pfxBytes);\n```\n\n### Loading Keys from Disk or Certificate Store\n\n```csharp\n// From PFX file\nvar bytes = File.ReadAllBytes(\"mycertificate.pfx\");\nvar certificate = X509CertificateLoader.LoadPkcs12(bytes, \"password\");\n\n// From certificate store\nvar store = new X509Store(StoreName.My, StoreLocation.CurrentUser);\nstore.Open(OpenFlags.ReadWrite);\nvar certificate = store.Certificates.First(c => c.Thumbprint == \"<thumbprint>\");\n```\n\n## Manual Key Rotation (Phased Approach)\n\nWhen using static keys, rotation must be performed carefully to avoid validation failures.\n\n### Why Phased Rotation is Necessary\n\n1. **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.\n2. **Existing tokens** - Tokens signed with the old key are still valid. Removing the old key immediately invalidates those tokens.\n\n### Phase 1: Announce the New Key\n\nSign with the old key, publish the new key as a validation key:\n\n```csharp\nvar idsvrBuilder = builder.Services.AddIdentityServer(options =>\n{\n    options.KeyManagement.Enabled = false;\n});\n\nvar oldKey = LoadOldKeyFromVault();\nvar newKey = LoadNewKeyFromVault();\nidsvrBuilder.AddSigningCredential(oldKey, SecurityAlgorithms.RsaSha256);\nidsvrBuilder.AddValidationKey(newKey, SecurityAlgorithms.RsaSha256);\n```\n\n**Wait:** Until all clients/APIs have refreshed their caches (default 24 hours).\n\n### Phase 2: Start Signing with the New Key\n\nSwap signing and validation keys:\n\n```csharp\nvar idsvrBuilder = builder.Services.AddIdentityServer(options =>\n{\n    options.KeyManagement.Enabled = false;\n});\n\nvar oldKey = LoadOldKeyFromVault();\nvar newKey = LoadNewKeyFromVault();\nidsvrBuilder.AddSigningCredential(newKey, SecurityAlgorithms.RsaSha256);\nidsvrBuilder.AddValidationKey(oldKey, SecurityAlgorithms.RsaSha256);\n```\n\n**Wait:** Until all tokens signed with the old key have expired (default access token lifetime: 1 hour).\n\n### Phase 3: Remove the Old Key\n\n```csharp\nvar idsvrBuilder = builder.Services.AddIdentityServer(options =>\n{\n    options.KeyManagement.Enabled = false;\n});\n\nvar newKey = LoadNewKeyFromVault();\nidsvrBuilder.AddSigningCredential(newKey, SecurityAlgorithms.RsaSha256);\n```\n\n## Migrating from Static to Automatic Key Management\n\nThis is also a three-phase process where automatic keys gradually replace static keys.\n\n### Phase 1: Enable Automatic Key Management, Keep Signing with Static Key\n\nThe static signing credential takes precedence over automatic keys. Automatic key management begins creating and announcing keys in discovery.\n\n```csharp\nvar idsvrBuilder = builder.Services.AddIdentityServer(options =>\n{\n    options.KeyManagement.Enabled = true;\n});\n\nvar oldKey = LoadOldKeyFromVault();\nidsvrBuilder.AddSigningCredential(oldKey, SecurityAlgorithms.RsaSha256);\n```\n\n**Wait:** Until all APIs and clients have updated their caches with the new automatic keys.\n\n### Phase 2: Switch to Automatic Signing, Keep Static for Validation\n\nRemove the static signing credential; keep it as a validation key:\n\n```csharp\nvar idsvrBuilder = builder.Services.AddIdentityServer(options =>\n{\n    options.KeyManagement.Enabled = true;\n});\n\nvar oldKey = LoadOldKeyFromVault();\nidsvrBuilder.AddValidationKey(oldKey, SecurityAlgorithms.RsaSha256);\n```\n\n**Wait:** Until all tokens signed with the old static key have expired.\n\n### Phase 3: Remove Static Key Entirely\n\n```csharp\nvar idsvrBuilder = builder.Services.AddIdentityServer(options =>\n{\n    options.KeyManagement.Enabled = true;\n});\n```\n\n## Multi-Instance / Load-Balanced Deployment\n\n### Requirements\n\n| Concern                             | Solution                                            |\n| ----------------------------------- | --------------------------------------------------- |\n| Key storage shared across instances | Use EF operational store or shared file system      |\n| Data protection keys shared         | Configure shared data protection key persistence    |\n| Key cache synchronization           | `PropagationTime` handles cache refresh windows     |\n| Initialization race condition       | `InitializationDuration` (5 min) allows server sync |\n\n### File System Store in Load-Balanced Environments\n\nAll instances need read/write access to the `KeyPath`:\n\n```csharp\noptions.KeyManagement.KeyPath = \"/shared-volume/identity-keys\";\n```\n\n### Recommended: Database-Backed Store\n\n```csharp\nbuilder.Services.AddIdentityServer()\n    .AddOperationalStore(options =>\n    {\n        options.ConfigureDbContext = b => b.UseSqlServer(connectionString);\n    });\n```\n\n## OIDC + SAML Shared Signing Keys\n\nWhen 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.\n\n### X.509 Requirement (SAML metadata needs certificates)\n\nSAML metadata requires X.509 certificates, not raw keys:\n\n- **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.\n- **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.\n\n### Constraints\n\n- The default SAML signing service supports **RSA only**. `UseX509Certificate` is **not** supported for EC (`ES`) keys.\n- For a different certificate, independent rotation, or an external key system, implement a custom `ISamlSigningService`.\n\n### Rotation Knobs (shared with OIDC)\n\n| Knob                | Effect for SAML                                                                                                          |\n| ------------------- | ---------------------------------------------------------------------------------------------------------------------- |\n| `PropagationTime`   | How long a new managed key is published before it starts signing — set long enough for all SPs to refresh IdP metadata. |\n| `RetentionDuration` | Keeps the previous certificate in metadata while SPs may still validate old messages (and old OIDC tokens remain valid). |\n\nService providers with **statically configured** IdP certificates must update those certs on every rotation.\n\n## Common Pitfalls\n\n1. **`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.\n\n2. **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.\n\n3. **Immediate key rotation** - Switching signing keys without a transition period causes validation failures. Use the phased approach or rely on automatic key management.\n\n4. **Disabling `DataProtectKeys` without alternative** - Turning off key encryption without ensuring your store encrypts at rest exposes signing keys to anyone with storage access.\n\n5. **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.\n\n6. **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.\n\n7. **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.\n"
}

SHA-256 of public snapshot: e614d5269d7d092b1e3c63d459e97438acff016fc349e655e0e61dff4b0cbd0d