Security Overview

How SikkerKey secures your secrets, where our responsibility ends, and where yours begins.

SikkerKey encrypts, stores, transports, and authenticates. You own everything the moment a secret leaves our API.

This page lists the technical controls SikkerKey runs on our side and marks the points where responsibility transfers to you. Each claim here reflects what SikkerKey actually does, not what we'd like it to do.

How SikkerKey Secures Your Secrets

Encryption at rest

Every secret value is stored under three layers of encryption.

  • Per-secret data key. Each secret value is encrypted with its own fresh 256-bit key under AES-256-GCM with a random 12-byte IV per write. The secret ID is fed in as Additional Authenticated Data (AAD), which cryptographically binds the ciphertext to the secret record. A ciphertext decrypted under the wrong secret ID fails the GCM tag check, so an attacker cannot swap one secret's ciphertext into another's record.
  • Per-project master key. Data keys are wrapped with a project-scoped master key. Projects in different vaults have independent master keys; a master key leak from one project reveals nothing about any other.
  • Decryption root. Master keys at rest are themselves encrypted with a decryption root that SikkerKey loads into memory at startup. The decryption root is never stored on disk, never logged, and never persisted anywhere in our infrastructure. It is held only in memory while SikkerKey is running, loaded fresh at the start of each service lifecycle, and never persisted to disk between sessions.

When a secret is decrypted, the master key is loaded into memory on demand, the data key is unwrapped, the plaintext is returned, and both the master key and data key are wiped from memory before the request returns. Plaintext secrets are never cached.

Passwords, TOTP seeds, and recovery codes at rest are hashed with Argon2id at OWASP-recommended parameters. Webhook signing secrets and sync credentials are encrypted at rest with AES-256-GCM using a separately derived key for each subsystem, so a leak in one area cannot decrypt the others.

Encryption in transit

  • DNSSEC signs the DNS records for our domain, so a validating resolver can confirm that the address it resolves for sikkerkey.com is authentic and not a spoofed or cache-poisoned answer pointing at infrastructure an attacker controls.
  • TLS 1.2+ is enforced at the edge by Cloudflare. Requests on lower protocol versions are rejected before they reach our servers.
  • HSTS on every production response, so browsers reuse HTTPS for one year after first contact.
  • Strict CSP on every production response with a per-request script nonce. Scripts that did not originate from our own build cannot execute in the dashboard, and our API responses are not embeddable.
  • Origin / Referer validation on every state-changing request rejects cookie-authenticated requests whose origin doesn't match the configured frontend hosts. Combined with SameSite=Lax cookies, this closes the CSRF surface.

Machine authentication

Machines authenticate to SikkerKey with Ed25519 request signatures, not bearer tokens. Every request is signed with the machine's private key. The signature covers the request method, URL, timestamp, nonce, and body, so any tampering invalidates it. The machine's private key is generated on the machine during bootstrap and never transmitted to SikkerKey. SikkerKey stores only the public key.

  • Signature verified before nonce consumed. An invalid signature never touches our nonce store, so an attacker cannot burn legitimate nonces.
  • Each nonce can be used only once. Replays are detected and rejected.
  • Timestamp window is 5 minutes past, 1 minute forward.
  • Rate limits track both the source IP and the machine identity, so an attacker can't sidestep them by rotating one or the other.
  • Query strings are rejected. The signed payload covers the path, not the query, so we refuse to trust any unsigned parameters tacked on after the fact.
  • Failure responses are generic. SikkerKey returns Authentication failed for every failure mode. The actual reason is recorded in the audit trail, but never disclosed in the response, so an attacker can't probe for what went wrong.

AI agent authentication

AI agents authenticate to SikkerKey with Ed25519 request signatures, the same primitive and replay-protection model as machines. The signed payload, timestamp window, nonce single-use, and rate-limit handling are identical to the machine flow.

Two things differ by design:

  • Agents and machines hit different route trees. Agent requests go to /v1/ai/... and are verified against the ai_agents table. Machine requests go to the SDK/CLI runtime routes and are verified against the machines table. The two tables are physically distinct, so an agent identity cannot satisfy a machine-auth lookup, and vice versa.
  • Agents have no plaintext-read route at the code level. Tools that look like reads (get, list, versions, rollback, dynamic_get) return metadata only, with no value field on the response struct. Tools that accept plaintext as input (create, update_value, rotate, dynamic_create) encrypt server-side and respond with metadata; nothing is round-tripped back. The runtime read surface is separate and bound to machine identities.

This lets you wire an AI client into your secrets infrastructure for management, code integration, and rotation without exposing plaintext to the agent. The full breakdown of scope catalog, project allowlist, audit attribution, temporary-secret delivery primitive, and revocation semantics is at the MCP security model.

Dashboard authentication

  • Argon2id password hashing at OWASP-recommended parameters.
  • Password policy rejects passwords under 12 characters, passwords without at least one digit and one non-alphanumeric character, a curated common-password list, passwords containing your username or email local part, and passwords with fewer than 5 distinct characters.
  • Per-(IP, email) login lockout on top of a per-account failure counter and per-IP rate limits, so distributed credential stuffing and spray both hit a cap.
  • Email verification codes and other verification secrets are stored as HMAC-SHA256 hashes with a server-side secret that lives only in memory, so a database leak alone does not yield rainbow-tablable hashes.
  • TOTP for optional two-factor authentication. Recovery codes are Argon2id-hashed and single-use. Every TOTP entry path (login, password-reset confirmation, first-time setup) is gated by a per-challenge attempt counter that locks the challenge after five wrong codes. A stolen password-reset link cannot be brute-forced through the 6-digit TOTP space across the token's lifetime, even from a distributed pool of source IPs.
  • Per-purpose signing keys. Tokens for different purposes (sessions, 2FA challenges, OAuth state, integrations) are signed with independently derived keys, so a key leak in one area cannot forge tokens for any other.
  • Access tokens have a 15-minute lifetime. Every verification checks that the session is still active, so revoking a session takes effect immediately.
  • Password change or 2FA toggle invalidates every existing session across every device, immediately.
  • Refresh tokens are single-use and rotated on every refresh. If a refresh token is reused after rotation — the canonical signal of token theft — we revoke every session for the account. The absolute session lifetime is 90 days for customer accounts, 30 days for employee accounts.
  • OAuth login is bound to the originating browser via a secure, HttpOnly cookie set when the flow starts. The callback only proceeds when the state and the cookie agree. A login link can't be redirected to a different browser to take over an account.
  • Session cookies are HttpOnly, Secure (in production), and SameSite=Lax. Authentication state is invisible to client-side scripts, so a stored XSS payload can't tell whether anyone is currently signed in.
  • Passkey support (WebAuthn). The dashboard supports passkeys for sign-in. A passkey can act as the second factor after a password, or replace the password entirely when the account opts into passwordless mode. Properties of the implementation:
    • The private key never leaves the user's authenticator. SikkerKey stores only the public key and a per-credential counter. There is no shared secret on the wire and no credential material at rest that an attacker could exfiltrate to forge a signature.
    • User verification (PIN, biometric, or whatever the authenticator requires) is mandatory at every registration and sign-in. A stolen hardware key cannot sign without it.
    • Every passkey ceremony's challenge is stored on our side with a single-use handle in the cookie. The challenge bytes never leave SikkerKey, and a finish attempt atomically consumes the challenge so the same handle cannot succeed twice.
    • Origins are exact-match. A passkey registered against the dashboard cannot be replayed from another site, and the browser itself will refuse to produce a valid signature for any origin other than the dashboard.
    • Each assertion's counter must strictly exceed the previous value. A counter regression is the canonical signal of a cloned authenticator; SikkerKey rejects the sign-in and writes a critical audit entry naming the affected passkey.
    • Destructive account actions (renaming or removing a passkey, changing the passkey policy, removing the password, linking or unlinking an OAuth account, disabling 2FA, disabling the IP allowlist, destroying the vault) require a fresh passkey verification immediately before the action. That fresh verification is single-use, expires within five minutes, and is cleared the moment the action completes.
    • Passkey sign-in, second-factor verification, step-up, signup, and recovery each carry their own per-IP rate limits with escalating lockout on abuse.
    • When the user opts into "require passkey at sign-in", TOTP and recovery codes are refused as completion factors. The only paths that mint a session are a successful passkey assertion or the dedicated recovery flow.
    • Recovery uses one of the codes generated at first passkey enrollment. Burning a recovery code wipes every passkey on the account, clears the passkey-required and passwordless flags, revokes every other session, and writes a critical audit entry. The user re-enrolls from a known-good device. A recovery code never reactivates the lost passkey, only opens the door to enrolling a new one. The recovery codes are the only self-recovery path on a passkey-required account: if both the authenticator and every recovery code are lost, the account cannot be restored by SikkerKey. This is by design. The alternative would be a side door that defeats the purpose of opting into passkey-only sign-in.
  • Enterprise SSO (SAML 2.0). Organizations can require members to sign in through their own identity provider. SikkerKey validates each assertion end to end: the signature against the configured IdP certificate, the audience and recipient, a time window, single-use replay protection, and binding to a sign-in request it actually issued (so unsolicited assertions are rejected). Accounts are provisioned just-in-time only for email domains the organization has verified by DNS. Enforcement is optional per organization; when it's on, password, passkey, and OAuth sign-in are refused for members, with a break-glass path for the owner. The identity-provider configuration is encrypted at rest, and changing it requires a fresh passkey step-up.

Authorization

Machine access to a secret requires all six of the following, checked on every single request:

  1. Valid Ed25519 signature over the request
  2. Machine is approved
  3. Machine is enabled
  4. Vault owner's account is active (not suspended)
  5. Machine is linked to the project the secret belongs to
  6. Machine has an explicit grant for that specific secret

No inheritance, no bulk access by default. Granting a machine access to a project does not grant it access to that project's secrets.

Dashboard access for organization members is enforced on every request against the member's capability template (what they can do) and project scope (which projects it applies to), plus the vault owner's account status. A member can act only within the capabilities their template grants and the projects they're scoped to; a member with no template can sign in but take no action.

Canary tripwires

Plant a canary alongside your real credentials in a project. The canary is indistinguishable from any other secret on the wire. When any machine reads it, SikkerKey freezes the project before the response returns: every subsequent read in that project from any machine fails with 423 Locked until the vault owner clears the freeze from the dashboard.

The freeze commits before the response is built. By the time the canary's reader receives a normal-looking 200 OK with the canary value, the frozen state is already in the database and canary_triggered and project_freeze audit entries are already written. A second request that arrives between the first canary read and the freeze gets 423 Locked rather than another secret value.

This bounds blast radius to one read by construction. The freeze lands before the response to the triggering read is returned, so the window for additional reads from the same machine is bounded by request latency, not by operator response time. A canary trigger can optionally extend to every other project in your vault that the offending machine is in, so lateral-movement containment is a configuration toggle.

The full mechanics covering arming, trigger configuration, recovery flow, and audit event catalog are at Canary Secrets.

Audit trail

Every privileged action is recorded in an append-only audit log. Append-only is enforced by the database itself, not by the application. Even an attacker with direct database write access cannot edit or remove audit entries — only the automated retention process can delete rows that have aged out per your plan's retention policy.

Audit entries are sanitized of control characters before storage, so attacker-controlled input like a machine name cannot forge lines in downstream log viewers.

See the full audit reference for every action type, its severity, and when it fires.

Tenant isolation

Every project has its own master key. No key material is shared across projects, regardless of which vault owns them. A customer never holds key material that could decrypt another customer's data, and we never decrypt one customer's data during a request scoped to another.

Membership is scoped to a single vault. A member of one organization has no visibility into any other vault, even when the same owner runs both; each member acts only inside the vault they select at sign-in.

How SikkerKey protects its own application secrets

SikkerKey's own application secrets are stored under the same envelope encryption as customer secrets, not in environment variables or config files. SikkerKey's operational credentials live in a dedicated vault on the same production database, encrypted per-project under a master key, which is itself wrapped by the decryption root. Without the decryption root, none of these secrets are recoverable from the database alone.

SikkerKey authenticates to its own vault with a dedicated machine identity using the same Ed25519 signature model customers use. Every read SikkerKey makes against its own vault is recorded in the audit trail, the same way customer reads are. There is no shortcut path that bypasses the vault: if SikkerKey can't read its own credentials, it doesn't start serving traffic.

The net effect is that SikkerKey's own application credentials sit behind the same locks customers pay for. A compromise of the database alone, without the decryption root, yields ciphertext for both customer secrets and SikkerKey's application credentials.

Defense in depth

  • SSRF guard on every outbound webhook delivery. Webhook URLs are checked against every reserved IP range that should never appear as a legitimate customer-supplied destination — private networks, cloud metadata endpoints, carrier-internal CGNAT, IETF and benchmarking allocations, and the IPv6 equivalents. Our HTTP client also pins the connection to the validated address, so an attacker can't change DNS between our check and our connection.
  • Rate limits cover login, registration, password reset, TOTP verify, email verify, machine auth, bootstrap, enrollment, webhook delivery, and a general API bucket. Failed machine-auth attempts lock out both the source IP and the machine ID after 3 failures in 5 minutes, for 30 minutes.
  • Request size limits are enforced before any handler runs. Requests that don't declare their size up-front are rejected.
  • Unhandled exceptions return Internal server error to the client. Our internal logs record only the exception class and top stack frame — never the exception message itself. Specific exception messages, which can include fragments of the request being processed, never enter the log pipeline. The concrete message stays in memory and is dropped with the request.

Backups

Database backups run daily at 03:00 UTC.

  • A compressed dump of the database is produced.
  • The dump is encrypted with AES-256-GCM under a dedicated backup encryption key, distinct from both the decryption root and the data encryption key. The backup key lives inside SikkerKey's own application-secret vault, encrypted under the decryption root. The decryption root is the only piece loaded into memory at startup; every other application secret is unwrapped from there.
  • A SHA-256 checksum is computed over the encrypted bytes.
  • The encrypted blob and checksum are pushed to a separate backup server over HTTPS. Every upload is cryptographically signed and verified by the backup server, with replay protection on each request. The backup server verifies the checksum after upload.
  • Every push attempt records its trigger, outcome, size, checksum, and timestamp.

The backup server holds only encrypted blobs. A breach of the backup server alone yields ciphertext that cannot be decrypted without the backup encryption key, and that key isn't held on the backup server and isn't reachable from any configuration file. It can only be decrypted by a running SikkerKey instance with the decryption root already in memory.

Hosting & Data Residency

  • Production runs on Hetzner bare-metal servers in Germany. No hyperscaler cloud, no US-hosted infrastructure, no multi-tenant VM slice.
  • Inbound traffic reaches SikkerKey only through a Cloudflare Zero Trust tunnel, established by the cloudflared daemon.
  • Production data at rest stays in the EU. The database in Germany and backups in Denmark sit under German and EU data-protection jurisdiction (GDPR, BDSG). No cross-border replication of stored data.
  • In-transit traffic passes through Cloudflare's global edge network. Cloudflare terminates TLS at the edge POP geographically closest to the requester, which may be outside the EU, and then proxies the request through the cloudflared tunnel to the German origin where the application processes it.
  • Backup storage runs on a physical server we own outright in Denmark, separate from any cloud provider or third-party datacenter. The backup encryption key isn't held on the backup hardware, so the backup blobs alone are unreadable.
  • Direct ownership of the backup hardware means restore-from-backup is an immediate action on our side, not a support ticket routed through a cloud provider's SLA.
  • SikkerKey is based in Denmark. The subprocessors required for the product (Cloudflare for TLS termination and edge protection, payments, email delivery) are listed at sikkerkey.com/sub-processors.
  • Customer IP addresses are never sent to a third-party geolocation service. The audit log's country and city columns come from a locally-hosted MaxMind dataset. Every lookup happens in-process and the IP never leaves SikkerKey's infrastructure.

What We Store vs. Never See

Stored on our sideNever on our side
Encrypted secret ciphertexts and wrapped data keysDecrypted plaintext secrets (decrypted in memory on demand, zeroed after use)
Machine public keys, registered during bootstrapMachine private keys (generated on the customer's machine, never transmitted to us)
WebAuthn credential public keys and per-credential signature countersPasskey private keys (held by the user's authenticator, never transmitted to us)
Your SAML identity provider's configuration (entity ID, sign-in URL, signing certificate), encrypted at restYour identity provider's private signing key (held by your IdP, never sent to us)
Email addresses, usernames, audit events, rate-limit stateThe decryption root on disk (held only in memory while SikkerKey is running)
Argon2id hashes of passwords and recovery codesPlaintext passwords or recovery codes
Stripe customer and subscription IDsPayment card numbers (held by Stripe)
Encrypted webhook signing secrets, TOTP seeds, sync credentialsThe decryption root in any persistent storage anywhere in our infrastructure
SikkerKey's own application secrets, encrypted under the same envelope as customer secretsSikkerKey application secrets in an environment variable or configuration file

Employee Data Access

SikkerKey commits to never building an employee route that reads, creates, modifies, rotates, or individually deletes customer secrets, and never building an employee route that manages, adds, approves, revokes, renames, or bootstraps a customer's machines. This is a standing policy, not a "for now" constraint. It is your data, not ours.

What employees can do

Through the internal operations portal, an employee can:

  • Manage user accounts, subscription plans, tickets, SLAs, and suspension categories.
  • Read a customer's audit log when working a support request. The audit log records what happened; it does not contain secret values, passwords, recovery codes, private keys, or session tokens.
  • See aggregate counts of a customer's projects, secrets, and machines. Names and counts, never values.
  • Delete a user account. This is a destructive cascade: the user's secrets, machines, project memberships, team invites, and audit history are removed along with the account. It is the only path by which an employee action can remove customer data, and it acts at the account level, not at the individual-secret or individual-machine level.

What no employee route can do

  • Read a customer secret's plaintext. No employee endpoint decrypts customer data, loads a project master key, or obtains any intermediate key material. The only routes that decrypt are the machine-authenticated endpoints, which require a customer-controlled Ed25519 signature.
  • Create, update, rotate, roll back, rename, or individually delete a customer secret. There is no "support write" path.
  • Approve, deny, revoke, rename, bootstrap, or add a customer's machine. Machine lifecycle is exclusively in the hands of the customer. Grants between machines and secrets are likewise untouchable by employees.
  • Impersonate a customer account. The one impersonation feature in the employee portal is hard-fenced to SikkerKey-operated internal vaults only. Every attempt to impersonate a customer account is rejected before any session token is minted, and the rejection sits alongside a permanent audit record. There is no path for an employee to act as a customer.

Why the commitment holds at the cryptographic layer

Even if an employee were granted direct database access, they obtain only ciphertext. Every customer secret is encrypted under a per-project master key that is itself wrapped by the decryption root. The decryption root is never stored on disk, never logged, and never reachable from an employee session. Decrypting customer data requires the decryption root in memory, and no employee credential, support tool, or internal flow produces it.

We commit to keeping it that way. If a support issue requires a customer's secret value, the customer hands it to us. We do not and will not reach into their vault.

Where Our Responsibility Ends

Three clear transition points. Responsibility transfers at each.

  • The API response leaves SikkerKey. The plaintext bytes are now in your process. What happens to them from that moment on is outside our visibility. This applies whether the caller is your CLI, your application's SDK, a CI/CD runner that bootstrapped a temporary machine identity at the start of its workflow, or any other authenticated client.
  • You add a member to your organization and assign them a capability template. They can then take the actions that template grants, in the projects they're scoped to. If that includes attaching machines, each machine they attach is another authenticated client subject to the first bullet: the API response leaves SikkerKey, the bytes are in its process.
  • A managed-secret agent completes a rotation. The ALTER ROLE runs, your database now holds the new password. We do not connect to, monitor, or see anything past that statement.

What's On Your Side

  • Your machine's private key file, generated on your machine during bootstrap
  • Your dashboard account credentials, including password and 2FA factor
  • Your passkey authenticators (hardware tokens, platform authenticators, password-manager-synced passkeys) and the recovery codes saved at enrollment. A lost authenticator with no recovery code stored is not recoverable by SikkerKey
  • Who you invite to your vault and what you grant them
  • If you use SSO: your identity provider and its configuration, and the DNS records that verify your SSO email domains
  • Your IP allowlist configuration, if enabled
  • Revoking machines through the dashboard when you decommission them