A very small number of ultra-privileged system administrators — in practice two or three people, never a team. Installer access is not application administration; it is effectively infrastructure administration for the whole host.
The reason is what the Installer can reach:
| Capability | Consequence |
|---|---|
| Mounts the host Docker socket | Access to the Docker socket is equivalent to root on the host |
Renders and writes the deployment .env | Every service credential in the deployment passes through it |
Reads system_secrets | It decrypts the Azure client secret and generated values |
| Runs a privileged, host-PID container for migration | That container runs the retirement script as root inside the host's namespaces |
| Reads the Jenkins configuration mount read-only | Legacy credentials in config.xml are readable through the import |
| Replaces the DataMind OS stack | It can stop, kill and recreate every service the business depends on |
Grant it the way you grant root: named individuals, no shared accounts, and only for as long as the work requires it.
Because the Installer drives the host's Docker daemon, and Docker daemon access is root access. The compose file mounts the socket in directly:
- ${DOCKER_SOCK:-/var/run/docker.sock}:/var/run/docker.sockAnyone who can ask that daemon to run a container with a host bind mount can read or write
anything on the host. The Installer itself does not exploit that, but the capability is inherent to
the design: an orchestrator that runs docker compose on your behalf must be able to. Treat
"can log in to the Installer" and "is a system administrator on this host" as the same statement.
Do not expose the Installer's port to the internet, and do not put it behind an authenticating proxy and treat that as the security boundary. The boundary is who holds an Installer account.
No. They are two entirely separate identity stores, with no shared accounts and no single sign-on.
The Installer keeps its own users table, signs its own session tokens with jwt.secret from its
own secrets volume, and has its own login endpoint. DataMind OS users belong to the DataMind OS
stack and are managed there.
The only bridge between the two systems is a shared service token:
| Direction | Mechanism | Scope |
|---|---|---|
| DataMind OS → Installer | CURATO_SERVICE_TOKEN, presented as a bearer token | First-party service call; treated as an administrator-level caller |
| Acting user's identity | x-actor-user-id and x-actor-email headers | Attribution only — used so the audit trail records a person; explicitly not what grants access |
Creating a user in DataMind OS does not create an Installer user, and deleting an Installer user does not remove any DataMind OS access. When someone changes roles, both stores need attention.
Two: admin and user. Every role check in the API names one of these.
| Role | Intended meaning |
|---|---|
admin | May change configuration, manage users, and start, stop, update or recreate the deployment |
user | May sign in and see status and logs; may not change anything about the deployment's configuration or lifecycle |
The interface hides or disables admin-only controls for a user, and the API enforces the same
rules independently — the decoration of a button is not the control.
More than "read-only" suggests, and one capability in particular deserves attention. A user
can call any authenticated endpoint that declares no role requirement:
Allowed for user | Not allowed for user |
|---|---|
| Read configuration (secret values masked) and the memory-limits calculation | Change any configuration value, generate .env, import from Jenkins, re-seed |
| Read deployment status, groups, stale services and the update check | Deploy, pull, compose up, restart, apply changes, stop, kill, cancel a job |
| Read the version and check for a newer Installer build | Read the deployment action audit trail |
| Stream container logs and job logs | Read or download the dedicated deployment-log feed |
| Read release notes | Manage users, roles, activation; set the Azure secret; set the VM user; run host migration |
| Change their own password |
Two consequences are worth stating plainly:
user can start an Installer self-update. The self-update endpoint requires authentication
but declares no role.user can stream container logs through the deployment log streams, which may contain
operational detail you would rather keep to administrators.If that is wider than your policy allows, keep the user role unused and give administrator
accounts only to the small set of people from the first question.
Register it from the login screen. The first account ever created is an administrator, and that can happen only once. The registration endpoint is public until any user exists, after which it is refused with a forbidden error and only an administrator can create accounts.
The password policy applies from the start: at least 8 characters, with an uppercase letter, a lowercase letter, a number and a symbol.
Sign in as an administrator and use the Register User view, choosing the Admin role. The role selector is only shown to administrators, and the API refuses account creation from a non-admin.
curl -X POST http://localhost:8000/api/user/register \
-H "Authorization: Bearer <access-token>" \
-H "Content-Type: application/json" \
-d '{"email":"ops@example.com","password":"<strong-password>","role":"admin"}'An administrator can also promote or demote an existing account, but cannot change their own role — that check exists so you cannot accidentally lock yourself out of administration.
Deactivate or delete the account, then rotate anything that person could have seen. In order:
encr.key, jwt.secret, the Azure client secret or the service
token, treat those as disclosed and rotate what you can. See the rotation questions in
Configuration and variables.Deactivation and deletion both revoke the account's refresh tokens, so existing sessions cannot be renewed. Access tokens already issued remain valid until they expire — which is the argument for short access-token lifetimes.
Set the account inactive; sign-in is then refused and existing refresh tokens are rejected. From the Users panel, or:
curl -X PATCH http://localhost:8000/api/user/<id> \
-H "Authorization: Bearer <access-token>" \
-H "Content-Type: application/json" \
-d '{"isActive":false}'A deactivated account gets a forbidden response at sign-in and its refresh attempts are revoked. Reactivating restores access immediately. You cannot deactivate your own account.
Yes — the refresh tokens are deleted outright, and the account is soft-deleted. A soft delete keeps the row (so the audit trail still resolves the user's email) while removing the account from the active list.
Registering the same email address later revives the row rather than creating a duplicate.
Yes. Access tokens default to 30 minutes and refresh tokens to 7 days, both configurable.
Sessions are cookie-based: an access token scoped to /api and a refresh token scoped to
/api/auth, both httpOnly, so script in the page cannot read them.
Tokens are not issued once and trusted forever: the refresh token is stored only as a hash, is rotated on every refresh, and is invalidated by a new sign-in, a password change, a password reset, deactivation or deletion. The strategy also rejects any token issued before the account's last password change.
Shortening access-token lifetimes is the cheapest way to bound how long a departed
administrator's live session can last. Set JWT_ACCESS_EXPIRY in the Installer's own .env and
recreate the backend.
In the deployment actions table, served as a paginated, administrator-only list.
curl -s "http://localhost:8000/api/deployment/actions?limit=50&offset=0" \ -H "Authorization: Bearer <access-token>"
Each row records the action, the target services, the job id, the outcome, the exit code, the detail and the acting user resolved to an email address. Actions include restart, stop, kill, pull, compose-up, deploy, apply-config, host migration, job cancellation and self-update.
The audit trail covers deployment actions. It is not a general access log: reads of configuration or logs are not recorded as actions.
With a shared service token generated by the Installer on first boot. The value is 48
alphanumeric characters, stored at delamain_secrets/curato.token, compared in constant time, and
injected into the deployment .env as CURATO_SERVICE_TOKEN on every render.
When a request presents that token, the Installer accepts it as an administrator-level service caller without requiring an Installer user row. The caller may forward the acting user's id and email in headers, which are used only to attribute the action in the audit trail.
The acting-user headers are not an authorization mechanism — the code says so explicitly. Do not build anything that treats a forwarded user id as proof of identity.
It is a long-lived bearer credential that bypasses user accounts and role checks. Anything that
can read the deployment .env — where the token is written beside the deployment's own service
credentials — can act on the Installer as an administrator. That includes starting a self-update.
Mitigations that exist in the design:
Mitigations that are yours to apply: treat the deployment .env as a secret file, restrict who can
read it on the host, and rotate the token deliberately if it is ever exposed.
No. Keep it on the management network, reached over SSH or a VPN. The Installer is an infrastructure control plane with Docker-socket privileges; the right answer is to make it unreachable from the internet rather than to harden it exposed.
Two supporting details: the interface is served by the Installer itself on the same origin as the
API, so there is no separate front-end host to secure; and the cookie Secure flag is set from the
request, not from a hardcoded assumption, so if you terminate TLS in front of the Installer the
protective flag is applied automatically.
If you do put the Installer behind TLS, verify it end to end. A cookie offered over plain HTTP
will be sent without the Secure attribute, and the interface will keep working — which hides the
problem until something inspects the traffic.
Stop the Installer, replace jwt.secret in the secrets volume, and start it again. The secret
is read at boot; there is no in-place rotation endpoint. Every issued token becomes invalid, so
everyone signs in again — which is exactly why this is the right action when you suspect a token
leak.
With the Installer's containers stopped, from the host:
openssl rand -hex 64 > <secrets-volume-path>/jwt.secret
The secret is not the same as the encryption key. Rotating jwt.secret invalidates sessions;
changing encr.key destroys stored secrets. Do not confuse the two.
No. The Installer runs its own PostgreSQL container and its own database. It is not published on
the host in the production compose file, and its password lives in the secrets volume rather than in
.env.
DataMind OS has its own databases inside the stack it deploys. Nothing in the Installer reads or writes those.
One thing only, and only on demand: the host-migration helper. Starting host migration copies
migrate-host.sh to the host mount and launches an ephemeral container with --privileged --pid=host that uses nsenter to run the script as root inside the host's namespaces.
The script acts on a fixed allowlist of legacy systemd units, plus NFS and stray old containers — it never touches Docker itself, the OS, or Jenkins. It is the one destructive operation in the product, it is administrator-only, and it is worth reading before you run it. See Migrating from bare metal.
Everything else the Installer does is ordinary docker compose work through the socket.
With AES-256-GCM under a key generated on first boot. Secret configuration values and
generated values are encrypted inside the Installer's database, and the system secrets table uses
the same scheme, with a version prefix on the ciphertext. The key itself lives in
delamain_secrets/encr.key, written with mode 0600.
In GET /api/configs, secret values are replaced with a fixed mask for any caller who is not an
administrator, so a user cannot read them even though they can read the rest of the configuration.
For how to protect and restore those files, see Data and backups.
The access model in full is in Who should have access, Identity and users and Host privilege and the Docker socket; Network exposure covers where the Installer should sit on your network.