GET STARTED

Install Zanskar

Zanskar runs as a single instance: one gateway with an embedded SQLite database (or PostgreSQL), plus a guacd sidecar if you want RDP or VNC desktops. Two supported install paths: the deb/rpm package on a Linux server, or the signed container image with Docker Compose. Both were exercised on a clean Ubuntu 24.04 machine before this release was cut.

Prerequisites

NeedPackage pathCompose path
HostLinux, amd64 or arm64, systemd. Debian/Ubuntu (.deb) or RHEL/Fedora/SUSE (.rpm). 1 vCPU / 1 GB is enough to start.Any Linux host with Docker Engine 24+ and the Compose v2 plugin.
NetworkUsers reach the gateway on 443 (80 redirects there). The gateway reaches targets on 22 (SSH), 3389 (RDP), 5900 (VNC), 5986 (WinRM), 5432 (PostgreSQL), 3306 (MySQL, MariaDB). Targets need no inbound internet exposure.Same, plus ports 80 and 443 on the host for Caddy.
TLSNothing to prepare: the gateway serves HTTPS with a self-signed certificate until you upload a real one on the Settings page (no restart). Or put a TLS-terminating proxy in front with init -behind-proxy. Zanskar never serves plain HTTP off loopback.Handled by Caddy: self-signed for localhost, or Let's Encrypt when you set a public hostname.
Desktops (optional)guacd 1.6+ reachable from the gateway. The package does not install it; the official container, started as shown in Start guacd, is the supported way. SSH, WinRM and database sessions do not need it.Included in the stack.
Databases (optional)Docker or Podman on the gateway host: database sessions run in ephemeral containers. See the guide.Not available inside the Compose stack; use the package path for database targets.
Verification (recommended)cosign to verify the release signatures. Not in the distribution repositories; step 1 installs it. sha256sum is on every Linux host already.

1. Install from the package

Download the package for your architecture, the checksums and the signature bundle from the latest release. Pick your distribution once; every block on this page follows. The commands use amd64; swap arm64 as needed.

sudo apt update && sudo apt install -y curl
# Work in /tmp: apt reads a local .deb as its unprivileged _apt user, which cannot
# enter a home directory (0750 on Ubuntu 24.04), and would print a "Download is
# performed unsandboxed as root" notice.
mkdir -p /tmp/zanskar && cd /tmp/zanskar
# Resolve the newest stable release, so these commands keep working.
V=$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/albatroxxx/zanskar/releases/latest | sed 's#.*/v##')
echo "installing $V"
B=https://github.com/albatroxxx/zanskar/releases/download/v$V
curl -LO $B/zanskar_${V}_linux_amd64.deb
curl -LO $B/SHA256SUMS
curl -LO $B/SHA256SUMS.sigstore.json

Install cosign first. It is not in the Ubuntu, Debian or RHEL repositories, so a fresh host answers cosign: command not found. Take it from the Sigstore project's own release (it resolves the newest version the same way):

C=$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/sigstore/cosign/releases/latest | sed 's#.*/v##')
curl -LO https://github.com/sigstore/cosign/releases/download/v$C/cosign_${C}_amd64.deb
sudo apt install ./cosign_${C}_amd64.deb
cosign version

On arm64 the files are cosign_${C}_arm64.deb and cosign-${C}-1.aarch64.rpm. Other platforms: Sigstore's installation page.

Verify integrity and origin. The signature is keyless Sigstore; the identity must be this repository's release workflow at this tag.

sha256sum -c SHA256SUMS --ignore-missing
cosign verify-blob --bundle SHA256SUMS.sigstore.json \
  --certificate-identity-regexp "^https://github.com/albatroxxx/zanskar/.github/workflows/release.yml@refs/tags/v$V\$" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com SHA256SUMS

Install. The package creates the zanskar service user, /var/lib/zanskar for the database and recordings, a hardened systemd unit, and an example environment file. It does not start the service, because there is no configuration yet.

sudo apt install ./zanskar_${V}_linux_amd64.deb
zanskar version

Configure with zanskar init

zanskar init writes /etc/zanskar/env (owned by root, readable by the zanskar group only, mode 0640), generates the master key, and prints the exact next commands with your paths filled in. Interactively it asks for the listen address, TLS mode, data directory, guacd, MFA and logging. Non-interactively it takes flags, which suits cloud-init and Ansible.

Interactive. Answers each question in turn; the defaults are the recommended setup.

sudo zanskar init

The default, non-interactively. HTTPS on 443 with a managed certificate (self-signed until you upload one in Settings), and plain HTTP on 80 redirected there.

sudo zanskar init -non-interactive

On a cloud instance. Name the address people will actually type. It is translated upstream, so the machine cannot find it and the certificate would not cover it (see below).

sudo zanskar init -non-interactive -tls-hosts zanskar.example.com

With desktops (RDP, VNC). Name guacd here, so the environment file never needs a hand edit. Start guacd itself as shown in Start guacd.

sudo zanskar init -non-interactive -tls-hosts zanskar.example.com -guacd 127.0.0.1:4822

Behind a TLS proxy (Caddy, nginx) on the same host.

sudo zanskar init -non-interactive -behind-proxy

With your own certificate files.

sudo zanskar init -non-interactive -listen 0.0.0.0:443 \
  -tls-cert /etc/zanskar/tls/cert.pem -tls-key /etc/zanskar/tls/key.pem \
  -guacd 127.0.0.1:4822

The flags combine: add -guacd 127.0.0.1:4822 to any of them for desktops, and -tls-hosts to any that uses the managed certificate (it names what that certificate covers, so it has no effect behind a proxy or with your own files).

Name the address people will use

The certificate Zanskar generates at first start covers what the machine can work out for itself: localhost, its host name, and the addresses on its network interfaces. On a cloud instance that is the private address. The public address is translated upstream and appears on no interface, so nothing can guess it.

Reaching a gateway by an address the certificate does not carry gives you two things, neither obvious:

So name it: the interactive zanskar init asks, -tls-hosts answers it non-interactively, and ZANSKAR_TLS_HOSTS in the environment file does the same afterwards. Several names are comma separated. Adding one and restarting regenerates the self-signed certificate so it carries it; an uploaded certificate is never touched. The Regenerate self-signed button on the Settings page does the same from the console.

Naming it removes the mismatch warning. The unknown-issuer warning stays until you upload a real certificate, which the same Settings page accepts and applies to the next connection without a restart.

Back up /etc/zanskar/env now. It holds ZANSKAR_MASTER_KEY, which seals every stored credential. It is never written to the database or to zanskar backup archives. Losing it makes every vaulted credential unrecoverable.

Apply migrations, create the first admin, start

Run the database commands as the service user so it owns the SQLite files it will write to. init prints this block for you. admin create asks for the password twice on the terminal without echo; do not put it on the command line, where it would land in your shell history.

sudo bash -c 'set -a; . /etc/zanskar/env; set +a; \
  runuser -u zanskar -- zanskar migrate && \
  runuser -u zanskar -- zanskar admin create --username admin --name "Your Name"'
# Password: ...        (typed at the prompt, never echoed, never in history)

sudo systemctl enable --now zanskar

# readiness check: pick the line that matches the mode you chose in init
curl -k https://localhost/readyz             # default: managed TLS on 443 (-k: self-signed until you upload one)
curl http://127.0.0.1:8443/readyz            # behind-proxy mode (plain HTTP on loopback)
curl -k https://localhost:8443/readyz        # own-certificate mode on 8443
# either way: {"status":"ready"}

In behind-proxy mode the gateway speaks plain HTTP on loopback and expects your proxy to terminate TLS on 443; probing it with https:// fails with an OpenSSL wrong version number error, which is expected, not a fault. journalctl -u zanskar -n 5 shows the listener and whether TLS is on. To switch modes later, re-run zanskar init -force with the new flags; the existing master key is kept.

Scripting the admin password. For cloud-init or Ansible, admin create also reads ZANSKAR_ADMIN_PASSWORD. Feed it from a prompt or a secrets manager, not a literal: read -rs ZANSKAR_ADMIN_PASSWORD && export ZANSKAR_ADMIN_PASSWORD, then run the command. If you did type a password into a shell, remove that line from history (history -d <n> or delete it from ~/.bash_history) and change the password after first sign-in.

First visit: check the certificate, then upload yours

A default install serves HTTPS with a self-signed certificate, so the browser warns on the first visit. Before accepting, compare the SHA-256 it shows with the one the gateway logged at start (journalctl -u zanskar | grep 'tls certificate'). Then sign in as the admin, open Settings → TLS certificate and upload a certificate your users' browsers trust (PEM chain and unencrypted key; it applies at once). Do this before inviting users: until then an on-path attacker present at someone's first visit could impersonate the gateway. Behind a proxy (init -behind-proxy) the proxy's certificate is what users see.

Start guacd, for RDP and VNC

Desktop sessions are relayed by guacd, which the package does not install. Without it, RDP and VNC targets fail with the desktop service (guacd) is not running, and the journal shows guacd dial failed. zanskar init checks the address you give it and prints this step when nothing is listening. The supported way is the official container, published on loopback only: guacd speaks an unauthenticated protocol and must never be reachable from anywhere but the gateway.

sudo docker run -d --name guacd --restart unless-stopped -p 127.0.0.1:4822:4822 \
  -e HOME=/tmp --read-only --tmpfs /tmp:size=512m,mode=1777 guacamole/guacd:1.6.0

Then point the gateway at it. init -guacd 127.0.0.1:4822 already did if you passed it. Otherwise add the line after zanskar init, never before: appending to a file that does not exist yet creates a one-line /etc/zanskar/env with no master key, the service restarts in a loop, and init then refuses to overwrite it. The block below checks first:

if sudo test -f /etc/zanskar/env; then
  sudo grep -q '^ZANSKAR_GUACD_ADDR=' /etc/zanskar/env \
    || echo 'ZANSKAR_GUACD_ADDR=127.0.0.1:4822' | sudo tee -a /etc/zanskar/env
  sudo systemctl restart zanskar
else
  echo 'run zanskar init first (add -guacd 127.0.0.1:4822 and it sets this for you)'
fi

HOME=/tmp matters: the image's user has no home directory and guacd's RDP client needs one for its certificate store. The read-only root and the tmpfs are the hardening the Compose stack and the AWS reference deployment use. Install Docker first if the host has none (apt install docker.io or dnf install docker, then systemctl enable --now docker); Podman works the same with podman run.

TLS without a certificate: Caddy and sslip.io

Zanskar refuses plain HTTP on a non-loopback address, so a server with no certificate needs one of two things: a reverse proxy that terminates TLS, or a certificate of its own. If the host has a public IP and ports 80 and 443 reachable from the internet, you can have a real Let's Encrypt certificate in a minute without owning a domain: sslip.io resolves 203-0-113-10.sslip.io to 203.0.113.10, and Caddy obtains and renews the certificate for that name automatically. This is what the AWS reference deployment does.

sudo apt update && sudo apt install -y caddy
# 1. your public IP, dashes instead of dots, becomes the hostname
IP=$(curl -s https://api.ipify.org); HOST=${IP//./-}.sslip.io; echo $HOST

# 2. Caddy terminates TLS and proxies to the gateway on loopback
sudo tee /etc/caddy/Caddyfile >/dev/null <<CADDY
$HOST {
  encode zstd gzip
  reverse_proxy 127.0.0.1:8443
}
CADDY
sudo systemctl enable --now caddy

# 3. the gateway runs in behind-proxy mode (plain HTTP on loopback, trusts Caddy)
sudo zanskar init -non-interactive -behind-proxy -guacd 127.0.0.1:4822
# ... then migrate, admin create, enable the service as above, and open https://$HOST/

Open TCP 80 and 443 to the world in your firewall or security group (80 is needed for the certificate challenge), and keep 8443 closed: the gateway listens on loopback only in this mode. The audit log records real client addresses because init -behind-proxy tells the gateway to trust Caddy's X-Forwarded-For. To use your own domain instead, point a DNS record at the host and put that name in the Caddyfile; nothing else changes.

Logs go to journald: journalctl -u zanskar -f. Then continue with first sign-in.

2. Install with Docker Compose

The single-node stack is the gateway on SQLite with a persistent volume, guacd for desktops, and Caddy terminating TLS in front. The gateway port is never published; only Caddy's 80 and 443 are. A one-shot zanskar-migrate service applies migrations before the gateway starts.

# Resolve the newest stable release, so these commands keep working.
V=$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/albatroxxx/zanskar/releases/latest | sed 's#.*/v##')
echo "installing $V"
git clone --depth 1 --branch v$V https://github.com/albatroxxx/zanskar.git
cd zanskar
cp deploy/.env.example deploy/.env
# edit deploy/.env:
#   ZANSKAR_MASTER_KEY=      -> output of: openssl rand -base64 32
#   ZANSKAR_VERSION=latest    -> optional; `latest` is the newest stable release
#   ZANSKAR_SITE_ADDRESS=    -> optional public hostname for Let's Encrypt

docker compose -f deploy/docker-compose.yml up -d
docker compose -f deploy/docker-compose.yml exec -it \
  zanskar /zanskar admin create --username admin --name "Your Name"
# Password: ...   (prompted twice, no echo; -it gives the command a terminal)

The image is ghcr.io/albatroxxx/zanskar, built for linux/amd64 and linux/arm64, running as a non-root user on a distroless base. Verify its signature the same way as the checksums; $V is the version the clone step resolved. cosign is in none of the Ubuntu, Debian or RHEL repositories, so on a fresh host install it first with the commands in step 1; the Compose path needs nothing else from that step.

cosign verify ghcr.io/albatroxxx/zanskar:$V \
  --certificate-identity-regexp "^https://github.com/albatroxxx/zanskar/.github/workflows/release.yml@refs/tags/v$V\$" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

Without a site address Caddy serves https://localhost/ with a self-signed certificate (a browser warning, fine for a trial). With ZANSKAR_SITE_ADDRESS set and ports 80/443 reachable from the internet, Caddy obtains a Let's Encrypt certificate automatically. Data lives in the zanskar_data volume.

3. First sign-in

  1. Open the gateway URL and sign in with the admin username and password you created.
  2. MFA is mandatory by default. Scan the QR code with an authenticator app and enter the code to enroll. If you refresh mid-enrollment the page resumes where you were.
  3. You land in the admin console. The how-to guide walks through credentials, targets, policies and the first session.

Lost the only admin's authenticator? On the gateway host: zanskar admin reset-mfa --username admin. The reset is audited and the next sign-in enrolls a new one.

Configuration reference

Install-time settings are environment variables: the package reads /etc/zanskar/env; Compose reads deploy/.env. After an edit the console shows Restart required and restarts the gateway with a drain. Runtime settings (login banner, the MFA requirement, guacd address, log level, the MySQL relay image), the TLS certificate and the recording storage are changed live on the console's Settings page, which shows the install-time value as read-only.

VariablePurpose
ZANSKAR_MASTER_KEYRequired, unless the next one is set. 32-byte base64 key that seals every stored secret. Generate with openssl rand -base64 32 or zanskar keygen.
ZANSKAR_MASTER_KEY_FILEA file holding the master key instead, so it is not in the environment file. It must be readable by its owner only (chown zanskar: , chmod 0400) or the gateway refuses to start; zanskar key rotate-master rewrites it in place. Set this or ZANSKAR_MASTER_KEY, not both.
ZANSKAR_LISTEN_ADDRListen address. Default 127.0.0.1:8443. Plain HTTP is refused on non-loopback addresses.
ZANSKAR_TLS_MODEmanaged (Zanskar serves TLS with a certificate it manages: self-signed at first start, replaced from Settings without a restart), file, or proxy. Unset: file when a certificate is set, else proxy.
ZANSKAR_TLS_CERT, ZANSKAR_TLS_KEYServe TLS directly from files. HSTS is sent automatically.
ZANSKAR_HTTP_REDIRECT_ADDRA plain-HTTP listener (the default install sets :80) that redirects every request to the HTTPS listener. The packaged unit may bind ports below 1024.
ZANSKAR_ADMIN_LISTEN_ADDROptional second listener for the admin console and API only, for a management network. Empty shares the main listener.
ZANSKAR_ALLOW_PLAIN_HTTPThrowaway installs only: allow the main listener without TLS on a non-loopback address.
ZANSKAR_TRUST_PROXY_TLS, ZANSKAR_TRUSTED_PROXIESBehind a reverse proxy: honour X-Forwarded-* from the listed CIDRs so the audit log records real client IPs.
ZANSKAR_CONSOLE_CLIoff removes the administrator console's command line (the terminal icon at the top right). On by default; it runs Zanskar commands only, never a shell. Every command.
ZANSKAR_DB_DRIVER, ZANSKAR_DB_DSNsqlite (default) or postgres with a connection string.
ZANSKAR_RECORDINGS_DIRWhere session recordings are written locally.
ZANSKAR_RECORDINGS_S3_BUCKET, _PREFIX, _REGION, _ENDPOINT, _KMS_KEY, ZANSKAR_RECORDINGS_SPOOL_DIRInstall-time default for an S3-compatible bucket (endpoint for MinIO or Ceph, KMS key for server-side encryption) and the local spool for recordings in progress. The console's Recording storage card can override the bucket, probe it and move existing recordings, without a restart.
ZANSKAR_GUACD_ADDRguacd host:port. Unset disables RDP and VNC. Install-time default; the console's Settings page can change it without a restart.
ZANSKAR_DOCKER_PATHContainer CLI for database sessions. Defaults to docker on PATH; set an absolute path or podman.
ZANSKAR_DBPROXY_IMAGEThe image run beside each MySQL or MariaDB session as the credential-holding relay: the gateway's own image at its own version by default. Install-time default; Settings can point it at a mirror.
ZANSKAR_AWS_GATEWAY_PRINCIPALOverride for the AWS principal named in autoscaling trust policies. Normally unset: on EC2 or EKS the gateway detects it and shows it on the Autoscaling page.
ZANSKAR_REQUIRE_MFADefault true. Only set false for throwaway installs. Install-time default; the console's Settings page can override it live, audited.
ZANSKAR_ISSUERName shown in authenticator apps. Default Zanskar.
ZANSKAR_SIEM_SYSLOG_ADDR, ZANSKAR_SIEM_SYSLOG_FORMAT, ZANSKAR_SIEM_SYSLOG_CA, ZANSKAR_SIEM_WEBHOOK_URL, ZANSKAR_SIEM_WEBHOOK_SECRETStream the audit log to a SIEM over syslog with TLS (cef, the default, or json; a CA file for a private authority) or as signed JSON to a webhook.
ZANSKAR_ADMIN_PASSWORD, ZANSKAR_NEW_MASTER_KEYCommand-line only: the password for a scripted zanskar admin create, and the next key for zanskar key rotate-master. Never in the service's environment file.
ZANSKAR_ENV_FILEPath of this file when it is not /etc/zanskar/env, so the console can show Restart required after an edit and restart the gateway with a drain of live sessions.
ZANSKAR_LOG_FORMAT, ZANSKAR_LOG_LEVELjson or text; debug, info, warn, error. The level is an install-time default the console's Settings page can raise live.

The full reference with every variable is docs/deploy.md in the repository.

Update to a new version

Migrations are forward-only and run automatically when the service starts, so an update is install-and-restart. Take a backup first; it is one command and runs while the service is up.

# package path: download and verify the new package into /tmp/zanskar exactly as in step 1, then
sudo bash -c 'set -a; . /etc/zanskar/env; set +a; runuser -u zanskar -- zanskar backup --out /secure/zanskar-$(date +%F).tar.gz'
cd /tmp/zanskar
sudo apt install ./zanskar_${V}_linux_amd64.deb   # or: sudo rpm -U zanskar_${V}_linux_amd64.rpm
sudo systemctl restart zanskar
zanskar version
# Compose path: set ZANSKAR_VERSION to the new release in deploy/.env, then
docker compose -f deploy/docker-compose.yml pull
docker compose -f deploy/docker-compose.yml up -d

Upgrades keep /var/lib/zanskar, /etc/zanskar/env and, on Compose, the zanskar_data volume. Read the release notes for anything version-specific. Live sessions get a grace period to close when the service restarts.

Restore from a backup

sudo systemctl stop zanskar
sudo bash -c 'set -a; . /etc/zanskar/env; set +a; runuser -u zanskar -- zanskar restore --in /secure/zanskar-2026-09-25.tar.gz --force'
sudo systemctl start zanskar

Restore refuses to overwrite an existing database without --force and warns when the archive was sealed under a different master key, because its stored credentials would be unreadable. Restore the original key first if you have it.

Remove Zanskar

Removing the package stops and disables the service and removes the binary and unit. It deliberately keeps the database, recordings, configuration and the zanskar user so a reinstall finds your data.

sudo apt remove zanskar

To purge everything, including every vaulted credential and every recording, run the following as well. This is irreversible; take a final zanskar backup first if there is any chance you will need the audit history.

sudo rm -rf /var/lib/zanskar /etc/zanskar
sudo userdel zanskar

Recordings and the audit log are compliance evidence. Check your retention obligations before purging them.

On Compose, docker compose -f deploy/docker-compose.yml down stops the stack and keeps the data volume; add -v to delete the volume as well.

Security

Keep ZANSKAR_REQUIRE_MFA=true. Terminate TLS at the gateway or on the same host. Never expose guacd (port 4822) beyond the gateway. Escrow the master key somewhere the database backup cannot reach. Ship the audit log to your SIEM and run zanskar audit verify on a schedule.

Releases are signed with Sigstore and every GitHub Action and base image is pinned by digest; the repository carries OpenSSF Scorecard and Best Practices badges and CodeQL runs on every change. Report vulnerabilities privately through GitHub private vulnerability reporting; the security policy commits to an acknowledgement within three business days.