Secret lifecycle
The hub private key, REALITY private key, SOPS age identity, Telegram token, provider private keys, VLESS UUIDs, subscription URLs, and generated device private keys are secrets. Public keys, device IDs, routes, and revision IDs are not secrets, though they can still reveal topology.
hubctl keygen writes a private hub key with mode 0600 and prints only the public half. hubctl device add --output … generates a client key and writes it into the profile with mode 0600; the CLI does not retain it in VPN Hub state, but the output file exists wherever the operator requested it. Remove temporary hub copies after SSH delivery. Losing that profile requires reissue.
The bot uses a different lifecycle: profiles it issues store the client private key as plaintext under /var/lib/vpn-hub/device-profiles/<device>.key, with a 0700 directory and 0600 file, so the same profile can be delivered again. Backups of state therefore contain client credentials. Revocation or reissue must remove the usefulness of a copied profile; filesystem permissions are protection, not encryption at rest.
Store provider files under /etc/vpn-hub and encrypt them with SOPS/age. The service resolves the age identity from SOPS_AGE_KEY_FILE; it is never accepted as a CLI flag. OpenVPN certificates, keys, and username/password pairs must be inline inside the SOPS-encrypted .ovpn content—external file and command references are rejected.
Subscription URLs are a different boundary: Viper reads them from root-only hub.yaml, and the hub configuration loader does not decrypt inline ENC[...] values. Keep /etc/vpn-hub/hub.yaml at 0600, restrict its backups, and rotate a provider URL or token after disclosure.
Next: Configuration overview.