Today, database connection details — including the password — are stored in plaintext, in a flat file on disk (config.json/config.toml), read directly by ConfigService::load (src/services/config.rs). To fully use the backup and restore capabilities exposed through the Portabase UI, the configured database user must be granted both read and write privileges on the target database (restore requires write access). This means the impact of this file being disclosed is not limited to read-only exposure: an attacker recovering these credentials gets full read/write access to the target database. Given that blast radius, storing this credential in plaintext on disk is not an acceptable risk in a production environment.
Describe the solution you'd like
Three directions, each removing the plaintext-on-disk requirement, with different trade-offs on where trust/custody sits:
- Field-level asymmetric encryption of the password (same spirit as sops/age, Kubernetes Sealed Secrets, or Ansible Vault): the agent exposes/derives a public key (e.g. from EDGE_KEY material, or a dedicated agent keypair generated on first start), and password in config.json holds ciphertext (e.g. enc:age1...) instead of the raw value. The agent decrypts in memory at load time using its private key — nobody can recover the plaintext from the file alone. Because it's ciphertext, config.json stays safe to commit to a config-management repo, which solves the versioning problem above — unlike a Docker secret, this remains diffable and reviewable.
- Externalized secret reference / secrets-manager integration: password becomes a reference (e.g. vault://secret/dn equivalent URIfor AWS Secrets Manager / Azure Key Vault / GCP SecManager) resolveablesecret-provider interface, instead of any value beiin the config filargerdeployments already running a secrets manager, and centralizes rota entirely.
- Split custody via the Portabase Server: the password is stored encryptede (the onebacking the UI) instead of anywhere on the agent's filesystem. confuld only needrouting/identification fields (host, port, username, database, genera. At runtime, the agent would fetch the password just-in-time from the Server API (authenticating agent_statuscall, via EDGE_KEY.agent_id), keeping it in memory only, never persisted xtension of thechannel that already pushes per-database
cron/storages/enlt.databases,src/services/api/models/agent/status.rs) — the same mechanism
could carry the nefit: no single leaked artifact (a copy of config.json, or a dump of the
Server's databasto reconstruct aworking credential — compromising either side alone isn't
enough. Trade-of hard dependencyon Server availability for every backup/restore run, where
today the agent local file; italso requires new server-side work (a secret-retrieval
endpoint) outsid
Options 1 and 3 ores/serves theciphertext, agent decrypts on receipt) rather than treated as mutually exclusithe currentplain password field for backward compatibility — existing deployments keep
Describe alternatives you've considered
Today, this can be worked around by mounting the config file through a Docker Swarm secret instead of a plain bind-mounted file. This works, but it has a real operational cost: a secret's content cannot be versioned, diffed, or tracked as regular configuration (git, config-management tooling, etc.) — that's the whole point of a secret. As soon as you manage more than a couple of databases across environments, you lose the ability to track "what database configuration is deployed where" the way you normally would for any other config file, and rotating/reviewing config changes becomes much more manual.
Today, database connection details — including the password — are stored in plaintext, in a flat file on disk (config.json/config.toml), read directly by ConfigService::load (src/services/config.rs). To fully use the backup and restore capabilities exposed through the Portabase UI, the configured database user must be granted both read and write privileges on the target database (restore requires write access). This means the impact of this file being disclosed is not limited to read-only exposure: an attacker recovering these credentials gets full read/write access to the target database. Given that blast radius, storing this credential in plaintext on disk is not an acceptable risk in a production environment.
Describe the solution you'd like
Three directions, each removing the plaintext-on-disk requirement, with different trade-offs on where trust/custody sits:
cron/storages/enlt.databases,src/services/api/models/agent/status.rs) — the same mechanism
could carry the nefit: no single leaked artifact (a copy of config.json, or a dump of the
Server's databasto reconstruct aworking credential — compromising either side alone isn't
enough. Trade-of hard dependencyon Server availability for every backup/restore run, where
today the agent local file; italso requires new server-side work (a secret-retrieval
endpoint) outsid
Options 1 and 3 ores/serves theciphertext, agent decrypts on receipt) rather than treated as mutually exclusithe currentplain password field for backward compatibility — existing deployments keep
Describe alternatives you've considered
Today, this can be worked around by mounting the config file through a Docker Swarm secret instead of a plain bind-mounted file. This works, but it has a real operational cost: a secret's content cannot be versioned, diffed, or tracked as regular configuration (git, config-management tooling, etc.) — that's the whole point of a secret. As soon as you manage more than a couple of databases across environments, you lose the ability to track "what database configuration is deployed where" the way you normally would for any other config file, and rotating/reviewing config changes becomes much more manual.