custoscp serve as a systemd service, with secrets held in systemd’s
encrypted credential store instead of a plaintext env file. The unit template is
the unit file below.
The control plane reads all config from environment variables (see Configuration for the full contract). Non-secret values go in the unit
as Environment=; secrets are encrypted at rest and decrypted into a per-service
tmpfs at runtime.
1. Service user
The unit runs as a dedicated locked system user. Missing it is thestatus=217/USER
failure — systemd aborts before ExecStart.
2. Generate keys
gen-keys prints an env-format bundle. Redirect to a root-only file so it never
hits terminal scrollback or shell history:
3. Encrypt secrets into the credential store
The idea is the same for every secret: put it in a file, encrypt the file, delete the file.systemd-creds encrypts with a host/TPM key only root can
access. Each output file in /etc/credstore.encrypted/ is found by its filename,
which must match the credential ID in the unit.
First make the store folder:
NAME=value lines. This loop
grabs each value (the part after =) and encrypts it:
- Secrets go into files, never onto a command line, so nothing lands in
shell history or
ps. Aftershred, the plaintext exists only in the running service’s memory. - A trailing newline from the editor is harmless: the service reads each value
with
$(cat ...), which strips it.
--name= credential ID = filename. Mismatch fails at service start.- Run
systemd-credsas root (sudo). Without it you getInteractiveAuthenticationRequired; if you instead putsudoon the wrong command in a pipe (e.g.sudo echo ... | systemd-creds), onlyechois root andsystemd-credsstill fails.
.cred files are ciphertext, tied to this host’s key; they do not decrypt
elsewhere.
4. Database migrations
migrate reads CUSTOS_DATABASE_URL. Decrypt it from the store straight into the
command’s environment — never onto the command line (which would hit history and
/proc/<pid>/cmdline):
decrypt call, not the URL; the value enters only
custoscp’s environment. Run this once before first start and after upgrades
that ship migrations. (Supabase on an IPv4-only host: use the session-pooler URI;
the direct endpoint is IPv6-only.)
5. First admin
Same pattern:6. Install and start
Per-deployment values
Edit these in the unit before installing:CUSTOS_APP_URL,CUSTOS_CORS_ORIGINS— the public frontend origin.CUSTOS_EMAIL_FROM— a Resend-verified sender.CUSTOS_LISTEN_ADDR— loopback; host Nginx terminates TLS and proxies here.OTEL_EXPORTER_OTLP_ENDPOINT— Alloy’s loopback OTLP receiver (http://127.0.0.1:4318). Because the CP runs on the host,127.0.0.1reaches Alloy’s published port. A containerized CP would usehttp://alloy:4318on the telemetry network instead. See the observability directory.
Rotating a secret
Put the new value in a file, re-encrypt over the same credential, then restart:The unit file
Install this to/etc/systemd/system/custoscp.service and edit the domain values in the
Environment= lines.