Key Vault
Compatible with the azure-keyvault-secrets and azure-keyvault-keys SDKs (Python, Java, JavaScript, .NET).
Self-signed certificate issuance also supports the Key Vault Certificates SDK.
Features
Secrets
- Secrets CRUD: set, get, delete, list secrets
- Versioning: each
set_secretcreates a new immutable version; latest pointer tracks the most recent - Soft-delete lifecycle: delete moves a secret to the deleted namespace; recover or purge it
- Properties update: update
content_type,tags,enabled,nbf,expwithout changing the value - Attributes:
enabled,not_before,expires_on; disabled secrets return 403 on get - Optional timestamps: unset
nbfandexpattributes are omitted from responses; supplied values remain numeric Unix timestamps - List operations: list active secrets, deleted secrets, or versions of a specific secret
- Optional trailing slash: fixed routes (
/secrets,/deletedsecrets,/certificates/contacts) accept a trailing slash, including .NETAddAzureKeyVaultconfiguration loading - Backup: backup a secret (base64-encoded blob)
- 32-char hex version IDs: matches Azure's version ID format
Keys & Cryptography
- Keys CRUD: create/import RSA (
RSA,RSA-HSM), EC (EC,EC-HSM, P-256/P-384/P-521), and oct (oct,oct-HSM) keys; get/list/list-versions; PATCH attributes - Soft-delete lifecycle: delete →
deletedkeysnamespace → recover or purge - Optional timestamps: unset
nbfandexpattributes are omitted from key responses; supplied values remain numeric Unix timestamps - Optional trailing slash:
/keysand/deletedkeysaccept a trailing slash, which the .NETKeyClientalways sends when listing - Backup/restore:
POST /keys/{name}/backupandPOST /keys/restore(see deviations below) - Rotation:
POST /keys/{name}/rotateand rotation-policy management (/keys/{name}/rotationpolicy) - Crypto ops:
encrypt/decrypt(RSA-OAEP, RSA-OAEP-256, RSA1_5; AES-GCM A128/A192/A256),sign/verify(RS256/384/512, PS256/384/512, ES256/384/512),wrapkey/unwrapkey /rng: random bytes for client-side key material (see deviations)- Managed HSM: the same data plane served under the
/{account}-managedhsm/suffix
Certificates
- Self-signed RSA and EC X.509 certificates with configurable subject, validity, key usage, EKUs, DNS and email SANs.
POST /certificates/{name}/createreturns a completed issuance operation. The SDK can poll/pendingand retrieve its target.- Get the current certificate or an immutable version, list certificates and versions, and read the issuance policy.
- Each certificate has matching
kidandsidversions, accessible through the existing Keys and Secrets SDKs. - Exportable certificates include the private key in a passwordless PKCS12 secret or PEM secret. Non-exportable certificates expose only the public certificate in the secret.
- Delete, recover, and purge preserve the certificate's versions and associated key/secret objects together.
External issuers, import/merge, policy/property updates, scheduled renewal, UPN SANs, and certificate backup/restore are not implemented.
Associated deleted key/secret objects are retained with the deleted certificate, rather than exposed through independent deleted-key/deleted-secret APIs.
Certificate private keys use the configured Key Vault storage backend and are not HSM-protected.
Issuance and deletion reject names containing independently created key/secret versions with 409 Conflict,
preserving those objects. Rotation policies on certificate-owned keys are retained across renewal and recovery.
Certificate lifecycle changes use atomic storage batches.
For example, with an already configured .NET CertificateClient and SecretClient:
var policy = new CertificatePolicy("Self", "CN=local-signing")
{
KeyType = CertificateKeyType.Rsa,
KeySize = 2048,
Exportable = true,
ContentType = CertificateContentType.Pkcs12,
ValidityInMonths = 12
};
var operation = await certificates.StartCreateCertificateAsync("local-signing", policy);
var issued = (await operation.WaitForCompletionAsync()).Value;
var secret = await secrets.GetSecretAsync("local-signing", issued.Properties.Version);
using var signingCertificate = X509CertificateLoader.LoadPkcs12(
Convert.FromBase64String(secret.Value.Value), "",
X509KeyStorageFlags.EphemeralKeySet);
Endpoint
Default account: devstoreaccount1
Default endpoint: http://localhost:4577/devstoreaccount1-keyvault
When TLS is enabled, host-based routing also works: https://{vault}.vault.azure.net and
https://{vault}.managedhsm.azure.net route to the matching Key Vault / Managed HSM when DNS
resolves those names to floci-az. These portless URLs target port 443, so publish or forward
port 443 as well as 4577 in Docker deployments: the emulator tries to bind 443 by default but
treats a bind failure as non-fatal. Clients that support an explicit endpoint port can instead
use https://{vault}.vault.azure.net:4577 and https://{vault}.managedhsm.azure.net:4577. The
self-signed certificate already carries the *.vault.azure.net and *.managedhsm.azure.net
SANs, so clients only need to trust the generated CA at
{persistent-path}/tls/floci-az-selfsigned-ca.crt.
Management plane
Microsoft.KeyVault/vaults and Microsoft.KeyVault/managedHSMs are ARM resources whose vaultUri and
hsmUri point at this data plane (see ARM). They persist under the global storage mode, in the
arm-keyvault store, so a vault created through ARM survives a restart under persistent or hybrid as its
secrets and keys do. The data plane does not consult them: it serves any vault name it is addressed by.
A vault or managed HSM name is a global DNS name: the first subscription to create it owns it, and another
subscription's create is refused (409 VaultAlreadyExists for a vault, 409 Conflict for a managed HSM) until the
owner deletes it. floci-az serves every vault on {name}.vault.azure.net and every HSM on
{name}.managedhsm.azure.net, keyed by name, so a name is unique across every configured cloud, not per cloud.
POST /subscriptions/{sub}/providers/Microsoft.KeyVault/checkNameAvailability and
.../checkMhsmNameAvailability answer from the same ownership. Paths that differ only in case address the same
resource. With parity-overrides.strict-names a name outside 3 to 24 letters, digits and hyphens, starting with a
letter, ending with a letter or digit and with no consecutive hyphens is refused with 400 InvalidResourceName (see
ARM name syntax).
PUT .../vaults/{name}/accessPolicies/{add|replace|remove} (Vaults_UpdateAccessPolicy, used by azurerm's
azurerm_key_vault_access_policy) updates the vault's properties.accessPolicies, which a GET of the vault returns:
add merges each entry's permissions into the entry with the same tenant, object and application ID or appends it,
replace replaces the permissions of the entry with the same IDs or appends it, leaving the vault's other entries
as they are, and remove takes the listed permissions away and drops an entry left with none. The
vault must exist in the request's own subscription and resource group. A vault PUT stores the
properties.accessPolicies it carries, and a re-PUT that omits the array keeps the vault's current list. Access policies are recorded, not enforced:
the data plane does not check them. Any other path under a vault or managed HSM answers 404.
Connection String
The Key Vault SDK enforces HTTPS and uses a challenge-based auth flow. Use the patterns below to connect to the local emulator:
import re, time
from azure.core.credentials import AccessToken, TokenCredential
from azure.core.pipeline.transport import RequestsTransport
from azure.keyvault.secrets import SecretClient
class FakeCredential(TokenCredential):
def get_token(self, *scopes, **kwargs):
return AccessToken("fake-token", int(time.time()) + 3600)
class ForceHttpTransport(RequestsTransport):
def send(self, request, **kwargs):
request.url = request.url.replace("https://", "http://", 1)
return super().send(request, **kwargs)
account = "devstoreaccount1"
endpoint = "http://localhost:4577"
vault_url = re.sub(r"^http://", "https://", endpoint) + f"/{account}-keyvault"
client = SecretClient(
vault_url=vault_url,
credential=FakeCredential(),
transport=ForceHttpTransport(),
verify_challenge_resource=False,
)
import com.azure.core.credential.AccessToken;
import com.azure.core.credential.TokenCredential;
import com.azure.core.http.HttpPipelineCallContext;
import com.azure.core.http.HttpPipelineNextPolicy;
import com.azure.core.http.HttpResponse;
import com.azure.core.http.policy.HttpPipelinePolicy;
import com.azure.security.keyvault.secrets.SecretClient;
import com.azure.security.keyvault.secrets.SecretClientBuilder;
import reactor.core.publisher.Mono;
import java.net.URL;
import java.time.OffsetDateTime;
static class FakeCredential implements TokenCredential {
@Override
public Mono<AccessToken> getToken(TokenRequestContext req) {
return Mono.just(new AccessToken("fake-token", OffsetDateTime.now().plusHours(1)));
}
}
static class ForceHttpPolicy implements HttpPipelinePolicy {
@Override
public Mono<HttpResponse> process(HttpPipelineCallContext ctx, HttpPipelineNextPolicy next) {
try {
URL url = new URL(ctx.getHttpRequest().getUrl().toString());
ctx.getHttpRequest().setUrl(
new URL("http", url.getHost(), url.getPort(), url.getFile()).toString());
} catch (Exception ignored) {}
return next.process();
}
}
String account = "devstoreaccount1";
String vaultUrl = "https://localhost:4577/" + account + "-keyvault";
SecretClient client = new SecretClientBuilder()
.vaultUrl(vaultUrl)
.credential(new FakeCredential())
.addPolicy(new ForceHttpPolicy())
.disableChallengeResourceVerification()
.buildClient();
Secrets
Set a secret
Get a secret
Delete a secret (soft-delete)
Recover a deleted secret
Purge a deleted secret
Versioning
Every set_secret call creates a new immutable version. The latest version is always returned by get_secret(name).
v1 = client.set_secret("db-password", "hunter2")
v2 = client.set_secret("db-password", "correct-horse-battery-staple")
# Latest is always v2
latest = client.get_secret("db-password")
assert latest.value == "correct-horse-battery-staple"
# Access v1 by version ID
old = client.get_secret("db-password", v1.properties.version)
assert old.value == "hunter2"
# List all versions
for props in client.list_properties_of_secret_versions("db-password"):
print(props.version, props.created_on)
Properties Update
Update metadata without creating a new version:
REST API Reference
All endpoints sit under /{accountName}-keyvault/ with an api-version query parameter.
Secrets
| Method | Path | Description |
|---|---|---|
GET |
/secrets |
List all secrets (properties only, no values) |
GET |
/secrets/{name} |
Get latest version |
PUT |
/secrets/{name} |
Set a secret (creates new version) |
DELETE |
/secrets/{name} |
Soft-delete a secret |
GET |
/secrets/{name}/{version} |
Get a specific version |
PATCH |
/secrets/{name}/{version} |
Update secret properties |
GET |
/secrets/{name}/versions |
List all versions |
POST |
/secrets/{name}/backup |
Backup a secret |
Deleted Secrets
| Method | Path | Description |
|---|---|---|
GET |
/deletedsecrets |
List deleted secrets |
GET |
/deletedsecrets/{name} |
Get a deleted secret |
DELETE |
/deletedsecrets/{name} |
Purge (permanently delete) |
POST |
/deletedsecrets/{name}/recover |
Recover a deleted secret |
Keys
| Method | Path | Description |
|---|---|---|
POST |
/keys/{name}/create |
Create a key (kty, key_size, curve, key_ops) |
PUT |
/keys/{name} |
Import a key (key JWK) |
GET |
/keys |
List keys |
GET |
/keys/{name} |
Get latest version |
GET |
/keys/{name}/{version} |
Get a specific version |
PATCH |
/keys/{name}/{version} |
Update key attributes (enabled, nbf, exp, tags) |
DELETE |
/keys/{name} |
Soft-delete a key |
GET |
/keys/{name}/versions |
List all versions |
POST |
/keys/{name}/backup |
Backup a key |
POST |
/keys/restore |
Restore a key from a backup blob |
POST |
/keys/{name}/rotate |
Rotate (regenerate) a key |
GET/PUT |
/keys/{name}/rotationpolicy |
Get/put the key rotation policy |
Deleted Keys
| Method | Path | Description |
|---|---|---|
GET |
/deletedkeys |
List deleted keys |
GET |
/deletedkeys/{name} |
Get a deleted key |
DELETE |
/deletedkeys/{name} |
Purge (permanently delete) |
POST |
/deletedkeys/{name}/recover |
Recover a deleted key |
Crypto Operations
| Method | Path | Description |
|---|---|---|
POST |
/keys/{name}[/{version}]/encrypt |
Encrypt (RSA-OAEP, RSA-OAEP-256, RSA1_5, A128GCM, A192GCM, A256GCM) |
POST |
/keys/{name}[/{version}]/decrypt |
Decrypt |
POST |
/keys/{name}[/{version}]/sign |
Sign (RS256/384/512, PS256/384/512, ES256/384/512) |
POST |
/keys/{name}[/{version}]/verify |
Verify a signature |
POST |
/keys/{name}[/{version}]/wrapkey |
Wrap a key (RSA-OAEP-256 for RSA keys) |
POST |
/keys/{name}[/{version}]/unwrapkey |
Unwrap a key |
POST |
/rng |
Random bytes ({"count": 1..128}) |
Intentional deviations
These are deliberate differences from real Azure Key Vault. They are stable, documented behavior, not bugs:
- Backup blobs are unencrypted plaintext. Real Azure returns HSM-encrypted opaque blobs that can only be restored into the same vault. floci-az emits a readable JSON snapshot (JWK + metadata) so backups are portable and inspectable. Do not treat backup blobs as secrets.
/rngcaps at 128 bytes per request. This matches real Azure (which caps at 128). An earlier draft of the plan stated 1024; that was incorrect.- Key material is stored in the clear in the storage backend (memory/persistent), not HSM-encrypted.
key_sizemay appear in the returned public JWK even though it is not part of Azure'sJsonWebKeyschema. Azure SDKs tolerate unknown fields; kept for convenience.nbf/expmust be numeric Unix epoch timestamps. Whitespace is tolerated and normalized; any other form is rejected with400 BadParameterat create/import/PATCH/restore time.
Storage Configuration
floci-az:
storage:
services:
key-vault:
# mode: persistent # override global storage mode
flush-interval-ms: 5000
services:
key-vault:
enabled: true
| Environment Variable | Default | Description |
|---|---|---|
FLOCI_AZ_SERVICES_KEY_VAULT_ENABLED |
true |
Enable/disable the service |
FLOCI_AZ_STORAGE_SERVICES_KEY_VAULT_MODE |
(global) | Per-service storage mode |