Advanced
In short. This page holds what you need only now and then: how the deploy is protected, what is where on the server, a private repository, updates to the server scripts, rare restores, login limits, and the security checklist. Do the security checklist before you go live.
How the deploy is protected
The design keeps the Session Key away from the computer you develop on, where coding agents run with your rights.- Signed tags only. Each
task deploy...command checks a release tag’s signature againstSESAME_ALLOWED_SIGNERS.task deploy -- <tag>checks the tag it installs. The other commands check the newestrelease-*tag on your computer. The command then runsdeploy/deploy.shfrom that tag, not the copy in your working tree. - The server checks again. The server checks the signature against
/etc/sesame/allowed_signers. It also checks that the signed tag has the name you asked for. - One command for each touch.
deploy.shmakes one SSH connection for each command. It uses no SSH agent and no forwarding, and the server’s host key must match the one you recorded. - No shell for the deploy key. The server sends every
deployconnection tosesame-deploy-shell. It accepts onlydeploy <tag>,rollback [tag],restore <name.db>,restore-stdin <name.db>,logs sesame|caddy,backup-now,list-backups,startandstatus. It records each request in the journal with the tagsesame-deploy-shell. Then it runssudo sesamectl <command> [argument]. - The deploy account is not root. Its one sudoers rule allows
sesamectland nothing else. It is not in thedockergroup, because access to Docker is root access. Do not listdocker composecommands in sudoers in place ofsesamectl:-f,run --entrypointandexecrun any command as root. - The server builds.
sesamectlbuilds both images fromgit archiveof the tag, so nothing from your computer gets to the server. It refuses to run while anyone other than root can write to/etc/sesame, a file in it, or/opt/sesame. A person who can writedeploy.conforallowed_signerschooses the code or the signer. - Secrets stay on the server.
sesame.envis root:root 0600. The deploy account cannot read it, and no command prints it. - Backups leave encrypted. The server encrypts each backup to your
agepublic key. The backup account can read only the encrypted copies. Neither machine has the key that decrypts them. - Computers with agents. Sign release tags only on a computer without agents. The person
who has the signing key decides what the server runs as root. If you must deploy from a
computer where agents run, use a security key with a PIN (
verify-required). A key file with a passphrase is not enough there. A process with your rights can read the file. It can also read the passphrase as you type it. On such a computer, the local tag check stops only an edited checkout. The protection comes from the server’s forced command and its own signature check. - What agents may do. Agents may change
Dockerfile,deploy/andTaskfile.ymlthrough a reviewed pull request. They never run deploys or open SSH to the server. They never use or read a key orsesame.env, and never sign or push release tags. They never edit/etc/sesame/*,SESAME_ALLOWED_SIGNERS,deploy/.deploy.env, git’s signing settings, sudoers, sshd or the firewall, and never add a proxy. - Lost deploy key. In the provider’s web console, delete
/etc/ssh/authorized_keys/deploy. If someone possibly used the key on the server, follow Session Key revocation and the incident runbook.
Files on the server
The app runs in two containers:
Both containers run as user 65532, with a read-only file system and no capabilities. Each
keeps at most five log files of 10 MB.
task deploy:logs shows the last 300 lines. For more,
run cd /opt/sesame && sudo docker compose logs sesame on the server. When the app stops, it
first finishes the requests that run. Docker gives it 30 s.
Private repository
If the repository is private, the server needs a key to read it.- In GitHub, open the repository settings, then Deploy keys. Add a key without write access.
- On the server, put the private key in
/root/.ssh. - Add
github.comto/root/.ssh/known_hosts. Use the fingerprints that GitHub publishes. - In
/etc/sesame/deploy.conf, writeREPO_URLin the formgit@github.com:<owner>/<repo>.git.
Updating the server scripts
A deploy never replacessesamectl, sesame-deploy-shell or the systemd files. When a
release changes deploy/sesamectl, the deploy prints:
sesame-deploy-shell or deploy/systemd/. Read the release
notes to know if they changed.
Install the new sesamectl from the tag that the server already fetched. On the server:
-
Copy the script out of the tag:
-
Read the script:
-
Install it and remove the copy:
sesame-deploy-shell, do the same steps with deploy/sesame-deploy-shell and
/usr/local/sbin/sesame-deploy-shell. For a file in deploy/systemd/, install it with mode
0644 in /etc/systemd/system/, then run sudo systemctl daemon-reload.
Backup details
- Every
BACKUP_INTERVAL(default 6 h), the app writessesame-<UTC time>.dbto/data/backups. It keeps the newestBACKUP_KEEPbackups (default 28). - Beside each backup, the app writes
<name>.mac. This is a checksum keyed fromSESSION_SECRET. It proves that this app wrote the backup. - A backup holds all the app’s data: the order audit trail, settings, alerts and login sessions.
- Every hour,
sesamectl seal-backupsencrypts each new backup and its.macinto/var/lib/sesame-backup/outbox/<name>.age. When the app deletes a backup, the encrypted copy goes too. - The encryption reads only regular files, as the app’s user. It skips files over 512 MiB.
- The outbox holds at most 128 files. When it is full, the encryption stops with an error (troubleshooting).
- A restore checks the MAC, the database’s integrity and its schema. It also checks that the backup’s audit trail is the start of the current audit trail.
- A restore deletes all login sessions and logs out every remembered browser.
Restore an off-site copy
Use this when the backups on the server are lost, or to restore an older copy. Do it on a trusted machine that has the backup keysesame-backup.agekey.
-
In the folder with the encrypted copies, check them against
SHA256SUMS:Each file showsOK. Do not use a file that showsFAILED. -
Decrypt the backup:
-
Decrypt its
.macfile: -
From the repository folder, upload and restore it. The
./tells the command to upload the file: - Delete the decrypted copies.
.mac file beside the .db file. The upload sends both. The server accepts at most
1 GiB and only these two names. It never writes over a file that exists. If the name is
already on the server, rename your file before the upload.
Restore onto a new server
A normal restore compares the backup with the history of the current data. A new server has no history, so the normal restore refuses the backup. For this case only, an administrator on the server runsrestore-fresh. It still needs the backup’s .mac to match the old
SESSION_SECRET. It refuses when the server already has order history.
-
Set up the new server with pages 1 and 2. Put
the old
SESSION_SECRETinsesame.env. -
Upload the decrypted backup and its
.macas in Restore an off-site copy. The server keeps the file but refuses the restore, because there is no history to compare. The app stays stopped. -
On the server, restore the file:
It prints
MAC verified; no current history to continue (...). It also prints the number of rows and the head of the order audit trail. Compare them with the last values the old server reported. The Telegram messageServer startedshows the head. - Log in and check the data.
restore-fresh there.
Backups that fail the MAC check
The MAC check fails for a backup written under aSESSION_SECRET that changed or was lost. It
also fails for a backup from a release older than the .mac files. sesamectl restores none
of these. Only an administrator at the server console can install one.
-
Check the encrypted copy against
SHA256SUMSbefore you decrypt it, as in Restore an off-site copy. Use the backup only if the check showsOK. -
Upload it with
task deploy:restore. The server keeps the file and refuses the restore. -
On the server, go to the app’s folder:
-
Stop the app:
-
Install the backup without the checks:
-
Start the app:
Login limits
- Each IP address gets 5 wrong passwords. An IPv6 address counts as its /64 network.
- After that, each wrong password locks that address out. The lockout starts at 1 minute and doubles each time, up to 15 minutes. After 1 hour with no attempt, the count starts again.
- A second limit covers all browsers that did not log in before, together: 20 login attempts a minute. A person with four or more new IPv6 networks a minute can keep new browsers out while this lasts.
- A browser that logged in before is remembered for 180 days. These limits do not stop it.
- Each time the second limit stops a browser, you get the notification
Logins refused for new devices. You get at most one each minute. - Log out everywhere and each restore make all browsers new again.
Security checklist
Do these checks on the real server after the first deploy. For each check, record the date, the release tag and the output. S1 to S9 must pass before you add the Session Key (S10).
S6, from a computer with IPv6:
/etc/docker/daemon.json does not set "ip6tables": false. Then deploy again. Until S6
passes, remove the AAAA record.
S7, the audit check on the server:
docker inspect commands show 65532:65532, true and [ALL]. The sesame container
also shows [no-new-privileges:true]. The caddy container also shows 268435456 128.
From your computer, the server refuses each of these commands:
journalctl -t sesame-deploy-shell lists the three refusals.
Last, do the proxy checks in
step 1 of Prepare the server
again.
Further reading
- configuration.md: every setting.
- Session Key setup, Telegram, live verification and incidents.
- The security reviews behind this setup: threat model, phase 6 findings and phase 6 recheck.