PostgreSQL
PostgreSQL databases on the VPS with their own least-privilege user, TLS and external access limited by an IP allowlist.
- Category: Servers and infrastructure
- Status: Available
- Requirements: An active VPS with the
postgresqlfeature available; Being a member with access to the VPS to install; admin to reveal the credential, open access or delete data - MCP tools (Read only):
ver_catalogo_apps,ver_preflight_app,ver_app,ver_acceso_app,ver_operacion_app,listar_motores_db_vps,listar_bases_db_vps - MCP tools (Write or modify):
instalar_app,revelar_credencial_app,cambiar_lista_ip_app,reintentar_operacion_app - MCP tools (Destructive):
revocar_acceso_app,desinstalar_app
What can you do with it?
Every PostgreSQL database is an app: it has its name, its own user with a random password and, if requested, internet access with TLS and an IP allowlist. The engine is installed once per VPS and shared by all its databases.
- Create several databases on the same engine, each isolated from the others.
- See the credential (user, password and connection URI) when needed.
- Open, change or close internet access by IP.
- Browse tables and data from the database manager.
- From the VPS PostgreSQL tab: list and create databases, users and permissions.
When should you use it?
- An application deployed on the same VPS that needs a relational database (it connects through
127.0.0.1:5432). - An external service (BI, another cloud, a workstation) that must read the database with TLS from known IPs.
How does it work in ArduMaker?
- Validate: active VPS and the
postgresqlsystem feature. - Engine: if not installed, it is installed once (distribution packages
postgresqlandpostgresql-contrib, random admin password stored encrypted). If it was already there, it is not reinstalled and neither its version nor its password changes. - Credential: an
app<id>_<hex>user is generated with a random password, encrypted on the platform before touching the VPS. - Role and database: the role is created
LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION; the database is created owned by that role, everything is revoked fromPUBLICand ahostssl <db> <role> … scram-sha-256line is added topg_hba.conf, with a reject below it for any other database. - Verification: the role cannot be superuser nor create databases or roles, it owns its database and
PUBLICcannot connect. If anything does not match, it fails withpermisos_no_verificados. - External access: only if requested with IPs. Engine TLS is enabled and the opening is requested as a separate operation; if it cannot open yet, the database stays installed anyway and the reason is recorded.
SQL travels encoded in the environment and enters through stdin; the password never appears on a command line or in a log.
config.external_access decides whether the database is reachable from the internet: {"ips": [...]} opens it only to those IPs; {"local": true} (or false) keeps it inside the VPS. If omitted, the dashboard uses the installer's IP; over MCP the agent's IP does not count, so without ips the database stays local instead of failing.
What actually changes?
| What | Detail |
|---|---|
| Engine | postgresql service enabled, listening on 127.0.0.1:5432 |
| Database | Name [A-Za-z_][A-Za-z0-9_]{0,62}, uppercase kept as is (quoted) |
| App role | app<id>_<hex> |
pg_hba.conf | Its hostssl block and a reject for every other database |
| TLS (if external access) | Own certificate in /etc/ardumaker/tls/, ssl=on, TLS 1.2 minimum, reload without restart |
Internet access and IP allowlist
By default the engine listens only inside the VPS (5432 on 127.0.0.1). To use the database from outside, ArduMaker publishes an endpoint of the engine: db-<VPS id> inside the platform zone (direct DNS record, no proxy), on a reserved port between 61000 and 64999. On the host, a proxy carries that port to the VPS 5432 and the firewall lets in only the IPs on the list.
The certificate belongs to that exact name (not a wildcard) and is installed in the engine: PostgreSQL turns on ssl with TLS 1.2 minimum and reloads its configuration without restarting. The app user only gets in over TCP with TLS (hostssl) and only to its database.
| List rule | Detail |
|---|---|
| Format | IPv4 or IPv6, with or without prefix; a single IP is /32 or /128. No names, loopback, link-local or multicast |
| Width | Minimum prefix /24 for IPv4 and /48 for IPv6 by default; 0.0.0.0/0 and ::/0 are rejected in the list |
| Size | Up to 20 entries; empty = closed to everyone |
| Host names | Up to 5 (allowed_hosts), with up to 8 public IPs each; re-resolved every 5 minutes, and a DNS failure keeps the last known value |
| Open to everyone | Only with allow_any and confirm_open together; the answer says so in open_to_any |
The list belongs to the engine, not to each database: changing it from any PostgreSQL database changes access for every database of that engine on that VPS (the answer says so with shared_with_engine and affected_apps).
- Missing firewall rules are added, one per CIDR (
ufw allow from <cidr> to any port <port> proto tcp). - Then the extra ones are removed and their open connections are cut.
- Only then is the listener created and verified: rules equal to the list, no rule open to anyone, listener present and a TLS handshake with the right name.
Uninstalling, persistence and backups
- Without
delete_data(the default): the app's own external access is closed and it stops being listed as installed, but the database, its user and its data stay on the VPS. The next reconciliation shows it as discovered. Nothing is lost. - With
delete_data: true(confirmed twice, admins only):DROP DATABASEand, if Apps created the user (app<id>_<hex>), also the role and its stored password. Never a customer user or the engine administrator. - The engine is never uninstalled from Apps: it stays installed even with no databases.
- If the VPS or the engine do not answer,
desinstalar_appwithforzar=truetries each step once, removes the app and lists innot_cleanedwhat was left on the server. - Data lives on the VPS disk and travels in its weekly backup, which is taken while the VPS is running. For a consistent copy of a specific database it is better to also make your own dump from the database manager (export) or with the engine's tools.
Diagram
- App on the same VPS:
127.0.0.1:5432 - External client:
db-<id>:<port>withsslmode=verify-full - Host firewall: only the listed IPs
- Host proxy: port 61000–64999 → VPS 5432
- PostgreSQL:
hostssl+scram-sha-256, its database only
Expected result
The database shows in Apps hanging from its engine. ver_acceso_app shows state, host, port, TLS and the applied list; revelar_credencial_app returns user, password and the URI. The URI only exists once external access is ready:
postgresql://app<id>_<hex>@db-<vps>.<zona>:<puerto>/<base>?sslmode=verify-fullErrors and troubleshooting
| Error | Cause | Retry? |
|---|---|---|
base_ya_existe | A database with that name already exists with another owner; it is not overwritten or adopted | No |
motor_detenido / motor_instalandose / motor_no_instalado | The engine is stopped, installing or could not be installed | Yes |
base_no_creada / permisos_no_verificados | The SQL failed or the role permissions did not verify | Yes |
lista_ip_invalida / lista_ip_demasiado_amplia | Malformed entry or range wider than the minimum; data.invalid says which | After fixing the list |
credencial_legacy / cuentas_del_motor_sin_verificar / tls_no_disponible | The engine does not yet meet the conditions to be exposed (old passwords, customer accounts not verified or TLS pending) | After resolving it |
acceso_externo_no_habilitado | External access is not enabled for that type in the company | No |
cierre_no_verificado | On closing, a rule or the listener remained; what is really there is reported | Yes |
State and steps of each operation: ver_operacion_app (including whether it is retryable). A failed retryable operation resumes from the failed step with reintentar_operacion_app, without reinstalling the engine. Asking for the list already applied queues nothing (idempotent: true).
MCP access
The whole database lifecycle can be operated over MCP with the permissions of the person who authorized the agent. Reference at /mcp/docs and /mcp/bases-de-datos.
- Read:
ver_catalogo_apps,ver_preflight_app,listar_apps_empresa,ver_app,ver_acceso_app(host, port, TLS and IP list, no secrets),ver_operacion_app. - Modify:
instalar_appwithapp_type: postgres(requiresidempotency_key),cambiar_lista_ip_app(only if its owner is an admin; IPs must be named explicitly: the agent's IP is not the person's),reintentar_operacion_app,refrescar_apps. - Sensitive:
revelar_credencial_app(admins only, noidempotency_key; the answer is markedsensible: trueand the audit records that it was revealed, never the value). - Destructive:
revocar_acceso_app(closes external access for the whole engine) anddesinstalar_app(withdelete_datait erases data; requiresconfirm_delete_data).
To read and write data inside the database, the agent uses the database manager tools. An agent authorized as read-only does not see the tools that modify and cannot reveal credentials.