Running Unlatch
Each release has static Linux binaries
for x86-64 and ARM64 and a container image. The release lists their SHA-256
sums, and gh attestation verify confirms that a file or image was built by the release workflow:
gh attestation verify unlatch-v0.1.0-x86_64-linux.tar.gz --repo Romanok2805/unlatch
gh attestation verify oci://ghcr.io/romanok2805/unlatch:v0.1.0 --repo Romanok2805/unlatch
The master key
Unlatch encrypts the secrets in its database with a master key, which lives in a file of its own. Create it once, readable by its owner only:
(umask 077; unlatch generate-key > /etc/unlatch/key)
Keep a copy apart from the database backups.
With systemd
The unit below runs Unlatch as a temporary user with no access to the rest of the system. The master key is passed as a credential, so it never sits in the service's environment or in its state directory next to the database.
[Unit]
Description=Unlatch single sign-on
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/unlatch
Environment=UNLATCH_ISSUER=https://login.example.com
Environment=UNLATCH_LISTEN=127.0.0.1:8080
Environment=UNLATCH_TRUSTED_PROXIES=127.0.0.1
Environment=UNLATCH_DATABASE=/var/lib/unlatch/unlatch.db
LoadCredential=key:/etc/unlatch/key
Environment=UNLATCH_KEY_FILE=%d/key
DynamicUser=yes
StateDirectory=unlatch
Restart=on-failure
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
SystemCallFilter=@system-service
CapabilityBoundingSet=
[Install]
WantedBy=multi-user.target
systemd reads the key, which can stay readable by root only, and hands it to the service.
In a container
The image holds nothing but the binary and runs as user 65532. It listens on port 8080 and keeps
its database in the volume /data. Releases publish it as ghcr.io/romanok2805/unlatch, tagged
with the version and latest; docker build -t unlatch . builds the same from source. Give it the
key as a read-only file that user can read:
chown 65532 /etc/unlatch/key
docker run -d --name unlatch -p 127.0.0.1:8080:8080 \
-e UNLATCH_ISSUER=https://login.example.com \
-e UNLATCH_TRUSTED_PROXIES=172.16.0.0/12 \
-e UNLATCH_KEY_FILE=/run/secrets/unlatch-key \
-v /etc/unlatch/key:/run/secrets/unlatch-key:ro \
-v unlatch:/data \
ghcr.io/romanok2805/unlatch
UNLATCH_TRUSTED_PROXIES names the network the reverse proxy connects from; the example covers
Docker's default networks.
Behind a reverse proxy
The proxy terminates TLS and forwards requests for the issuer's host to Unlatch. List its address
in UNLATCH_TRUSTED_PROXIES, so Unlatch sees the real client addresses in X-Forwarded-For.
With Caddy:
login.example.com {
reverse_proxy 127.0.0.1:8080
}
Backups
Back up the database and the master key, separately. The database can be copied while Unlatch runs:
sqlite3 /var/lib/unlatch/unlatch.db "VACUUM INTO '/var/backups/unlatch.db'"
Before an upgrade changes the database, Unlatch saves a copy next to it, such as
unlatch.db.v1-1792600000.bak, named after the old schema version and the time.