KEY CONCEPTS

The keys, pins and tokens that hold Zanskar together

Zanskar keeps a small number of secrets and a small number of pins. Each one answers a specific question: who is the gateway, is this the same machine as last time, is this browser really the signed-in user. This page says what each is, where it lives, what happens when it changes, and what to do when it leaks.

Master key

ZANSKAR_MASTER_KEY is a 32-byte key that lives only in /etc/zanskar/env (or your Compose .env), or in a file of its own named by ZANSKAR_MASTER_KEY_FILE and readable by the service user alone. It never enters the database. It wraps the data keys that seal every stored credential: passwords, SSH private keys, certificate-authority keys, cloud access keys. This is envelope encryption (ADR 0007): the database holds data keys encrypted under the master key, and each secret is encrypted under a data key, tagged with the key version that sealed it.

ExternalId, for autoscaling groups

When Zanskar assumes a role in your AWS account to list an autoscaling group, it presents an ExternalId: a random value generated per group at enrolment and written into that role's trust policy. It stops the confused-deputy attack, where a third party who knows your role ARN tricks a shared service into using it. The value is shown in the group's IAM setup panel; it is not a secret in the strict sense, but it belongs in the trust policy and nowhere else.

SSH host keys

Every SSH target has a pinned host key. The first probe captures the key and marks it pending; an administrator trusts it after checking the fingerprint out of band (the cloud console's serial output, the machine's own ssh-keygen -l, or the person who built it). Connections are refused until then, and refused again if the key ever changes: a changed key marks the target changed, writes a target.hostkey.changed audit event, and needs a fresh trust decision. Autoscaling instances are pinned the same way per instance, with the key source (serial console or trust-on-first-use) recorded.

RDP and WinRM certificate pins

Windows targets are pinned by the SHA-256 fingerprint of the RDP listener certificate and, separately, of the WinRM HTTPS listener certificate (ADR 0012). The probe captures both; a desktop or PowerShell session verifies the pin at the gateway before anything is relayed, and refuses on a mismatch. Self-signed certificates are fine because the pin, not a certificate authority, is what is trusted. When a certificate is renewed, probe the target again so the new fingerprint is pinned; until then connections fail with a pin mismatch. WinRM needs an HTTPS listener on 5986, which Windows does not create by default; the guide has the script.

An autoscaling instance is pinned the same way but per instance, and without an administrator in the loop, because the machine appeared minutes ago and may be gone within the hour (ADR 0024). The sync loop keeps whichever certificate an instance first presented. If a later poll sees a different one on that same instance, the pin is not replaced: the instance is marked unhealthy, the change is audited, and its sessions end. So a certificate renewed in place takes an instance out of the pool until the group replaces it. That is trust on first sight, and weaker than a fingerprint a person checked, which is why Windows instances are expected to come from an image that already carries its listeners.

Vaulted credentials

A credential is how Zanskar authenticates to a target: a password, an SSH private key, an SSH certificate authority, a Windows domain account, or EC2 Instance Connect (which stores no secret at all). The secret is sealed under the master key and is never shown again, never sent to a browser, and never written to a recording. A credential is bound to a target per protocol; a user who connects gets a session, not the secret. Rotate a credential from the Credentials page; every target bound to it uses the new material from the next session. Deleting a credential is refused while a target still binds it.

The SSH authority key

An SSH certificate authority credential holds one private key, sealed like every other secret, and is the recommended way to reach Linux targets (ADR 0022). Each target trusts its public key through one TrustedUserCAKeys line. For every session Zanskar mints a throwaway key pair and a certificate signed by the authority: valid for exactly one login user, for minutes, permitting a PTY and nothing else, with a key id zanskar:<zanskar user>:<login user>:<time> that sshd writes to the target's auth log. Nothing per target is stored anywhere, so there is nothing to rotate on a host.

The TLS certificate

A managed install (ZANSKAR_TLS_MODE=managed, the zanskar init default) serves TLS with a certificate the gateway keeps in its database (ADR 0021): a self-signed one made at first start for the listen host, the machine's name and its addresses, replaced by whatever you upload on the Settings page. The private key is sealed under the master key like a credential. The listener asks for the certificate on every handshake, so an upload serves on the next connection and removing it falls back to the file or the self-signed one, no restart. The SHA-256 logged at start and shown in Settings is what to check on the first visit, when the browser warns about the self-signed certificate. In file mode the certificate comes from ZANSKAR_TLS_CERT and ZANSKAR_TLS_KEY; in proxy mode it is the proxy's business.

Session cookie and CSRF token

Signing in sets one cookie: HttpOnly, Secure, SameSite=Strict, holding a random session id whose hash is in the database. Every state-changing request must also carry X-CSRF-Token, a value derived from that session that the browser app receives after sign-in and that a foreign site cannot obtain. A request without it is refused with 403 csrf_invalid; a request whose Origin is not the gateway's own host is refused the same way, which is why a reverse proxy must pass the original Host through. Sessions expire on their own, end on sign-out, and can be revoked for a user by an administrator (Revoke sessions on the user's row). A sign-in ends after 60 minutes without activity and after 12 hours at most. Admin and auditor accounts must hold a second factor.

Connect tickets

Opening a session is two steps. POST /connect evaluates every policy, pin and grant and, if all pass, returns a ticket: a one-time token valid for a few seconds, bound to the requesting address. The browser then opens the WebSocket with that ticket as its only credential. A ticket cannot be reused, cannot be used from another address, and carries no target credential; the gateway looks the credential up itself when the ticket is redeemed. invalid or expired ticket therefore means the WebSocket came too late, twice, or from a different address than the request, most often through a proxy that does not forward the client address consistently.

Recordings and the audit chain

Every session is recorded, terminal as asciicast with the submitted command lines as markers, desktop as the guacd instruction stream. A finished recording carries its SHA-256, and every audit event is hashed onto the one before it (ADR 0008), so a row that is altered or removed breaks the chain, which zanskar audit verify and the audit page report. Reading the audit log and playing a recording are themselves audit events, whoever does it.

What a backup holds

zanskar backup writes a consistent snapshot of the database and the recordings directory while the service runs. It contains users, groups, targets, policies, sessions, the audit log, recordings that are on local disk, the sealed credentials, the sealed TLS and authority keys, and the wrapped data keys. Recordings in an S3-compatible bucket stay in the bucket; back that up with the bucket's own tooling. It does not contain the master key, the TLS certificate, or /etc/zanskar/env. A complete restore therefore needs the archive, the master key, and the environment file. Test a restore on a scratch host before you need one.