Skip to content
sitesolide

Docs Secrets

Secrets

API keys, tokens, password hashes: what must never enter git.

The machine holds them, and nothing else does but its backups. /etc/sitesolide on the VM is the vault, and systemd reads each project's files from it with EnvironmentFile= when the service starts. The dashboard manages every file there, from its Secrets section: you read a value, set or remove a variable, create a declared file that is missing, restore the previous version, and restart the service with a verdict. A token minted with Manage secrets sets and removes the variables of <slug>.env from a workstation or an agent's sandbox, sitesolide secrets set NAME, and never reads one back: see access.md.

The backups keep a copy, so that a lost machine does not take its secrets along: every hour, /etc/sitesolide goes into the server's restic repository with the machine's own state, then to the bucket if one is configured, encrypted by restic before it leaves. No page lists or restores that copy; its files are put back by hand (dashboard/src/backup/README.md, "The machine's own state"). Whoever reads dashboard-backup.env, the owner from the Secrets page, holds the passphrase and the bucket's keys, and so every file of that copy, the password hashes and the DNS token the page itself never shows included.

No copy of production waits on your workstation, in a repository or beside it. sitesolide deploy never pushes a secret: it checks that each file the manifest declares is on the machine, and when one is not, it stops and names the dashboard where to create it. By then the manifest is deposited, so the dashboard already knows the file belongs to the site.

A manifest declares the project's own files alone. The unit reads every file it declares as root, whoever owns it: a cloned repository deployed over SSH that declared dashboard-backup.env would hand its code the backups' keys, one that declared cms.env the secrets of cms. So deploy refuses, before anything is pushed, a file that is not <slug>.env or <slug>-<name>, one of the machine's own, one a more specific site deployed there holds, and one already on the machine that is not a plain file of site-<slug>, reading its owner without opening it. A token's deployment is held to <slug>.env alone, and the installer refuses the same files, secret-taken. sitesolide remove deletes only the files that are the project's by that rule, and says which it kept.

WhoTheir projects' filesThe machine's own
The owner, with the dashboard's passwordread, write, create, restore, change a password hashthe same: dashboard.env, portal.env, dashboard-monitor.env, dashboard-backup.env
An Admin of the projectread, write, create, restorenothing
A Developer of the projectwrite without reading back: set, replace or remove a variable, replace a file read whole, create a declared file. No value, no size, no previous version ever comes back to themnothing
A token with Manage secrets, a person's token bounded by the person's role, whoever minted itlist the files and variable names; set, replace and remove the variables of <slug>.env alone, and create it, when the deployed manifest declares it. Never a value, a size or a previous version back, whoever holds it, and no unlock askednothing
A Viewer, or someone who can only open the sitenothing: the Secrets section is not theirsnothing

The steward decides every one of these, as root, on its own access registry: a request for a value from a Developer is refused before any file is opened, and a compromised dashboard, which relays it, cannot make it pass. Nobody but the owner ever changes a password hash, which takes the dashboard's own password, nor touches a file of the platform's projects. A Developer or an Admin unlocks by signing in again with the identity provider, never with a password, for ten minutes: see access.md and dashboard/README.md.

Where a secret lands, and to whom it belongs

Link to the section Where a secret lands, and to whom it belongs

From its name and its site, with no file to keep up to date:

Path/etc/sitesolide/<name>
Ownersite-<slug>, the account the project's service runs as
Mode0600 for an environment file, 0400 for a file read whole, 0444 for a .pub

A file belongs to a site by its name: <slug>.env, <slug>-<name>, or <slug>-secrets/<name> in a single-level subdirectory. When two sites could claim a name, cms-tool.env with both cms and cms-tool deployed, the most specific one wins. The machine's own files, dashboard.env, dashboard-monitor.env and dashboard-backup.env, are the dashboard's alone, whatever site their name happens to start with: a project deployed over SSH as dashboard-backup would otherwise list the backups' keys in its own Secrets, for its Admins to read. The landing is served from the directory named after the zone, but its files and its account carry the landing label: landing-mail.env is its.

The steward, the root daemon that does the writing, brings two sources together:

  • the secrets of an app's sitesolide.json: these can be created from the dashboard even before they exist;
  • the files already present in /etc/sitesolide that carry a site's name: that is how a site no manifest describes, the landing or a service made by hand, keeps its files in the dashboard.

A file no manifest declares is managed once present, and never created from the dashboard. Otherwise a compromised dashboard could lay an environment file down for a service that does not expect one yet. To add such a file, create it empty on the machine, then fill it from the dashboard:

ssh you@your-machine 'sudo install -m 600 -o site-landing -g site-landing /dev/null /etc/sitesolide/landing-mail.env'

The owner and mode of a present file are compared exactly: more open, it is no longer a secret; more closed, its service would stop reading it. Either way it is listed as unmanaged, with the command that repairs it.

A file created from the dashboard, or by a token, starts with no previous version. The steward drops whatever it kept under that name before it creates the file: a site removed and created again under the same name may be given its predecessor's uid, useradd handing the freed one out again, and a restore would otherwise hand it its predecessor's secrets.

A value systemd refuses never lands. Besides one line, no null byte and valid Unicode, a value may hold no Unicode noncharacter, U+FDD0 to U+FDEF or the last two code points of a plane: systemd reads a file with one as invalid, and starts the service with none of its variables, the other lines' included.

RuleFilesWhy
write-only: replaced, never read back, size not shownfiles read whole and closed to other accountsa private key never comes back to the screen, and its size would already give away its algorithm
Change password only: never read, never set by hand, never restored, the old one erasedany PASSWORD_HASH, in any filea hash is cracked offline, and restoring would revalidate the password you changed because it had leaked
hash only: no other variabledashboard.env, portal.envone more variable there would change what the service does: a socket path would send the next password typed to another socket

What belongs to no project is not the dashboard's: cloudflare.env, the token Caddy reads for its certificates, lives in /etc/caddy and is changed on the machine. Nor is the key people sign in to the dashboard with: the steward draws it and lays its private half in /etc/sitesolide-portal/, where only the portal's account reads it and the Secrets section never shows it, see dashboard/README.md.

No secret is shared, and that is the rule: the shared service api/ sends no mail and makes no call. Each file carries its project's name, so that each project has its own credentials, under its own account, in the same directory without ambiguity.

dashboard.env belongs to root, 0600, not to site-dashboard like the others. The dashboard's service still receives it, since EnvironmentFile= is read by PID 1 as root before the service takes its identity. In exchange, a compromised dashboard cannot rewrite the hash that unlocks the secrets.

dashboard-backup.env, the bucket's settings and keys, is root's 0600 too, handed by PID 1 to the backup run, which reads there the bucket, the retention, the disk reserve and nothing that moves a program or a folder: whoever holds the dashboard's unlock writes it, and is not root on the machine. See dashboard/src/backup/README.md.

dashboard-monitor.env, the monitor's alerting addresses, is root's 0600 as well, and listed under the dashboard's site. PID 1 hands it to the monitor; the dashboard's service never reads it, so a compromised dashboard cannot learn the heartbeat's address without an unlocked session and ping it to hide that the machine is down. Create it empty once, then fill it from the dashboard: see monitor/README.md.

It is the one secret the dashboard cannot create for itself, being what opens it. On a machine that has none yet:

bin/dashboard-password.sh

It draws a password, shows it once on your terminal, and puts its hash on the machine straight from memory: nothing is written on the way. Store the password before closing the terminal. Once the dashboard runs, its password changes from the dashboard, through Change password in the dashboard site's own Secrets. If it is lost, bin/dashboard-password.sh --replace.

~/.config/sitesolide/secrets/ holds what your workstation itself presents to reach production, and only that: the API token a command-line tool sends to one of your services, for instance. sitesolide run -- <command>, from a project's folder, loads the files its manifest declares from there and runs the command:

cd my-tool && sitesolide run -- bun scripts/sync.ts

It is not a copy of production. A value only the machine uses stays on the machine, and one the workstation keeps has two copies to hold equal: change it in the dashboard, then in that file.

The folder is outside every repository, beside the configuration file, so no git add can ever publish it. Claude Code is denied reading and writing there, see .claude/settings.json.

The Hetzner token sitesolide machine uses and the Cloudflare token sitesolide setup uses are not the machine's secrets but yours, and sitesolide keeps neither on your workstation: each is read from an environment variable or from standard input for the length of one command, never from an option, which the shell's history and ps would show. Keep them in your password manager.

The Cloudflare token also lives on the machine, in /etc/caddy/cloudflare.env, root:caddy 0640, where Caddy reads it for the wildcard certificate: setup lays it there. Restrict it to the one zone, Zone / Zone / Read and Zone / DNS / Edit, so that the machine holds no more than it needs. See machine.md and setup.md.

Up to 0.2 both lived in ~/.config/sitesolide/terraform/terraform.tfvars; upgrading.md says what to do with that folder.