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.
Who reads and who writes
Link to the section Who reads and who writes| Who | Their projects' files | The machine's own |
|---|---|---|
| The owner, with the dashboard's password | read, write, create, restore, change a password hash | the same: dashboard.env, portal.env, dashboard-monitor.env, dashboard-backup.env |
| An Admin of the project | read, write, create, restore | nothing |
| A Developer of the project | write 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 them | nothing |
| A token with Manage secrets, a person's token bounded by the person's role, whoever minted it | list 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 asked | nothing |
| A Viewer, or someone who can only open the site | nothing: the Secrets section is not theirs | nothing |
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 belongsFrom its name and its site, with no file to keep up to date:
| Path | /etc/sitesolide/<name> |
| Owner | site-<slug>, the account the project's service runs as |
| Mode | 0600 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
secretsof an app'ssitesolide.json: these can be created from the dashboard even before they exist; - the files already present in
/etc/sitesolidethat 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.
Three guard rails
Link to the section Three guard rails| Rule | Files | Why |
|---|---|---|
| write-only: replaced, never read back, size not shown | files read whole and closed to other accounts | a 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 erased | any PASSWORD_HASH, in any file | a hash is cracked offline, and restoring would revalidate the password you changed because it had leaked |
| hash only: no other variable | dashboard.env, portal.env | one 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.
The dashboard's own password
Link to the section The dashboard's own passworddashboard.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.
What your workstation keeps
Link to the section What your workstation keeps~/.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 tokens that create the machine
Link to the section The tokens that create the machineThe 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.