DOCS / ARCHITECTURE & SECURITY

Korta design

Korta provisions databases and applications on hosts you already have: virtual machines, cloud instances and physical servers. The only requirement is SSH access with root or passwordless sudo. Korta does not need an agent, Docker or Kubernetes on the hosts.

Goals

  • Enterprise teams declare what should run (a PostgreSQL server, a Node.js backend, a Next.js frontend) and Korta converges hosts to it.
  • Software runs natively under systemd, installed from official package repositories.
  • Secure defaults: TLS everywhere, SCRAM passwords, private CA, encrypted secrets and backups.
  • Every change is an idempotent, recorded operation that can be retried.

Not goals for v1: container orchestration and autoscaling. Creating machines is limited to EC2, from roles (see AWS instances).

Architecture

 web console / API clients
            │ HTTPS: session cookie or bearer token
            ▼
 ┌──────────── korta-server ────────────┐
 │ REST API · operations runner         │
 │ deployment types (postgres, ...)     │
 │ vault (AES-256-GCM) · private CA     │
 └───────┬───────────────────┬─────────┘
         │ state             │ SSH (pinned host keys)
         ▼                   ▼
   state.json / vault   instances: sh -s as root via sudo
Concept Meaning
Instance An SSH-reachable host with a vault SSH key. Its host key is pinned at registration or on first connection
Deployment A typed workload (postgres or mysql) on one or more instances, with a validated spec
Operation One recorded run (check, apply, backup, restore) with steps. One runs at a time
Type Code that turns a spec into shell scripts and runs them. New types do not change the core

One-call provisioning

POST /api/provision takes a host (or an instance ID), an SSH key (or a vault key ID) and a deployment. It runs one operation: trust the host key, detect the OS, then apply the deployment. Repeating the request converges, so the same call works for API clients, a CLI, CI pipelines and AI assistants (MCP).

Host key trust

Without a supplied key, Korta pins the key the host presents on first connection (trust on first use) and refuses any later change, which protects every connection after the first. Re-pinning after a reinstall is an explicit action. Supply the key up front, from the cloud provider or image build, when the first connection must also be verified.

Agentless execution

Each step opens an SSH session and sends a generated script on stdin to sh -s, through sudo -n for non-root users. Scripts never appear in process lists. stderr is merged into stdout, and the last 20 lines are attached to a failed step.

Scripts are idempotent: applying the same spec twice changes nothing except regenerated values noted below.

Deployment types

type Type interface {
    Name() string
    Normalize(spec, instances) (spec, secrets, error) // validate, default, extract secrets
    Apply(ctx, env) error                            // converge every instance
    Action(ctx, env, name, args) (any, error)        // backup, restore, status, ...
}

Every spec value that reaches a shell script or SQL statement is validated against a strict pattern first. Secret values are passed only inside quoted heredocs.

PostgreSQL (v1)

One server per deployment, PostgreSQL 14 to 17 from the official PGDG repositories.

Area Behaviour
Hosts Ubuntu 22.04/24.04, Debian 12/13, RHEL/Rocky/AlmaLinux 8 to 10, CentOS Stream 9/10; amd64 and arm64
Repositories PGDG on all hosts. On RHEL-family hosts PGDG uses its major-version paths (CentOS Stream has no minor releases), and backups add EPEL and CRB
Configuration Managed xela.conf included last from postgresql.conf; memory tuned from host RAM; extra settings allowed
Restarts Reload by default; restart only when PostgreSQL reports pending_restart
Roles Login roles with generated 256-bit passwords. Only SCRAM verifiers are sent to the host
Databases Created with an owner; CONNECT revoked from PUBLIC and granted to the owner
Network Listens on localhost unless access rules exist. Each rule becomes a hostssl ... scram-sha-256 line plus a firewalld or ufw rule
TLS Server certificate issued by Korta's private CA (397 days, renewed within 30 days of expiry). Apps use sslmode=verify-full with the CA
Backups pgBackRest to local disk or S3-compatible storage, AES-256 encrypted, WAL archiving, full weekly and differential daily by systemd timers, first full backup on enable
Restore Latest or a named backup set, or point in time. Requires confirm set to the deployment name

Korta never drops roles or databases removed from a spec, and deleting a deployment leaves software and data on the host.

Example

curl -H "Authorization: Bearer $TOKEN" -d '{
  "name": "orders-db", "type": "postgres", "instanceIDs": ["ins_..."],
  "spec": {
    "version": 17,
    "roles": [{"name": "orders"}],
    "databases": [{"name": "orders", "owner": "orders"}],
    "access": [{"database": "orders", "role": "orders", "cidr": "10.0.0.0/16"}],
    "backup": {"repo": "s3", "s3": {"endpoint": "s3.eu-west-1.amazonaws.com", "bucket": "acme-pg", "region": "eu-west-1", "accessKeyID": "AKIA...", "secretAccessKey": "..."}}
  }}' http://127.0.0.1:8080/api/deployments

MySQL (v1)

One server per deployment, MySQL 8.4 LTS (default) or 9.7 LTS from Oracle's official repositories.

Area Behaviour
Hosts RHEL, Rocky, AlmaLinux and CentOS Stream 8 to 10 on amd64 and arm64; Ubuntu 22.04/24.04 and Debian 12/13 on amd64 only, because Oracle publishes no arm64 Debian packages. Oracle's package names are installed explicitly and the version is checked, so a distribution MySQL is never used by mistake
Repositories Oracle's apt or yum repository. The apt signing key is refreshed on every apply, merged with the keyserver copy, because Oracle extends the same key's expiry
Administration Korta takes over root@localhost from the package's initial login (socket on Debian/Ubuntu, temporary password on RPM) and sets a generated password, kept in the vault and in /root/.xela-mysql.cnf (0600)
Configuration zz-xela.cnf, included from the main my.cnf (Korta adds the include on Oracle's RPM, which lacks it). After a restart Korta checks that the running server uses its port, bind address and certificate
Users Generated 256-bit passwords. Passwords reach the host in SQL over SSH stdin; MySQL redacts them from its logs. Owners get all privileges on their databases
Network Listens on localhost unless access rules exist. Each rule creates 'user'@'cidr' (IPv4) requiring TLS, granted only the databases that user owns. Removing a rule drops that account
TLS Server certificate from Korta's CA. Apps verify with ssl-mode=VERIFY_IDENTITY and the CA certificate
Backups Daily encrypted (openssl enc AES-256, key in the vault) mysqldump of the application databases to a local directory, with binary logs kept for the retention period. Korta's own statements are not written to the binary log
Restore Latest, a named dump, or a point in time: application users are locked and disconnected, the databases are dropped and reloaded from the dump, binary logs are replayed to the target, then users are unlocked

MySQL backups stay on the host in v1; point the directory at a mounted network share for off-host copies. Off-host physical backups (Percona XtraBackup to S3) are on the roadmap.

Roles

A role is a named, versioned list of typed steps applied to instances selected by ID or label. The engine is the same agentless Go executor as deployments: each step renders an idempotent shell script from validated values (file contents and custom scripts travel base64-encoded) and reports ok, changed, skipped or reboot.

Step Manages
packages present, absent or latest, with apt or dnf
patch security or all updates, with exclusions; reboot never, if-required or always, then waits up to 10 minutes for the host
file content, owner, group and mode, written atomically; or removal
service started or stopped, enabled or disabled, optional restart after earlier changes; aliases such as sshd resolve to the real unit
sysctl settings in /etc/sysctl.d/90-xela-<role>.conf
user account, shell, supplementary groups and the exact authorized SSH keys; removal keeps the home directory
firewall a port for given networks with firewalld or ufw
command a custom root script, skipped when creates exists; flagged high-risk

Plan, approve, apply. A plan runs every step in report-only mode on every target. A successful plan is recorded with the role's content hash. Applying requires approve, the ID of the newest plan for the current content (under 24 hours old), and confirmCommands when the role has custom scripts. Applying invalidates the role's plans and is recorded in the audit log. Hosts run batchSize at a time; after a failed batch the rest are skipped unless continueOnFailure is set.

Role assistant

Operators can draft or revise roles in conversation with Claude (Claude Opus 5.5 through the Anthropic API, high effort, with the API's server-side refusal fallback enabled).

  • Claude has one tool, propose_role, whose input is the role schema. It cannot run commands or reach hosts.
  • Korta decodes every proposal strictly and runs Normalize, plus a check that instance IDs exist. Errors go back to Claude to fix (at most four rounds); only valid drafts reach the operator.
  • The first message carries the inventory as data: instance IDs, names, labels and operating systems, and existing role names. No addresses, keys, tokens or passwords.
  • Conversations are kept in the manager's memory for 24 hours, append-only. Saving stores the draft as a role with source: "ai" and an audit entry; applying it needs the same plan and approval as any role.
  • Without KORTA_ANTHROPIC_API_KEY (or ANTHROPIC_API_KEY) the feature reports itself as disabled.

Known limits: security-only patching needs advisory metadata, which CentOS Stream does not publish (use all); removing a sysctl or firewall step does not undo it.

SSH console

The console page (/console.html) runs xterm.js, vendored with checksums in internal/api/web/vendor/xterm. It connects to GET /api/instances/{id}/console over a WebSocket; the manager opens a login shell with a pseudo-terminal through golang.org/x/crypto/ssh, as the instance's SSH user and against its pinned host key (requesting that key type, since hosts offer several). Instances whose key is not yet pinned are refused.

  • Only users with instance.console on the instance (operator or higher), and only same-origin WebSocket handshakes.
  • Idle sessions close after 30 minutes and every session after 8 hours.
  • Opening, closing and failures are recorded in the audit log; terminal contents are not recorded.
  • The console page allows inline styles (xterm.js creates style elements); scripts stay limited to the manager's own files there too, and the rest of the console keeps a strict policy.

Applications (Node.js and Next.js)

Types node and nextjs deploy an application from a Git repository to 1-20 instances with the same CPU architecture and package family.

Area Behaviour
Source HTTPS (or HTTP) Git URL and a branch, tag or commit. A private repository's token is sealed in the vault and reaches git as an http.extraHeader through environment configuration, never in a URL, argument or config file. Shallow fetch, with a full-fetch fallback for servers without smart HTTP
Build On the first instance, as the app's own unprivileged account, with git and compilers installed there only, so native modules match the hosts. Default commands: npm ci (or npm install without a lockfile), npm run build when defined, npm prune --omit=dev. Next.js standalone output is packaged with its static files when present
Node.js Official nodejs.org build of the chosen LTS major (22, 24 or 26; default 24), verified against SHASUMS256. Only the build host downloads it; other hosts receive the verified archive through the manager and need no internet access
Release <UTC time>-<commit> archive with a SHA-256 checksum, copied through the manager and verified on every host, extracted to /srv/xela/<name>/releases/<release>
Runtime systemd unit xela-app-<name> running as xela-<name> with NoNewPrivileges, PrivateTmp, ProtectSystem=full, ProtectHome. PORT, NODE_ENV=production and HOSTNAME (127.0.0.1, or 0.0.0.0 with access networks) are set; other variables come from /etc/xela/apps/<name>.env (0640, root and the app account)
Rollout batchSize instances at a time: switch the current symlink, restart, and probe healthPath (any status below 500) for up to 90 seconds. On failure the instance returns to its previous release, the failed release is deleted, and the rollout stops. The newest keepReleases releases stay for rollback
Database link Names a PostgreSQL or MySQL deployment, user and database; injects the URL (default DATABASE_URL), DATABASE_CA_FILE and NODE_EXTRA_CA_CERTS pointing at Korta's CA, so verified TLS works with common drivers
Actions status, logs, restart and rollback; a rollback picks one release for every instance and stops at the first failure

Access control

People sign in as users. Each user holds grants; a grant is an access level, either everywhere or scoped to instances whose labels include the scope's labels (env=staging). Deployments are covered when all their instances are, and roles when their targets are: listed instances must be covered, and a label selector must include the scope, so instances labelled later stay inside it.

Level Permissions it adds
viewer read
operator instance.check, instance.console, deployment.operate (apply, backup, logs, restart, rollback), role.edit, role.plan
approver role.approve, deployment.restore, deployment.credentials
admin deployment.manage; global only: instance.manage, sshkey.manage, secret.write, audit.read
owner access.manage (users and grants). Owner grants have no scope

Levels include the ones before them. The global-only permissions reach hosts, keys and secrets that no label can contain (a scoped admin could otherwise relabel a production host, or point a production key at a new address), so scoped grants never confer them.

  • Enforcement. Every API route checks its permission. Lists show only what the caller can read, and anything else answers 404, so scoped users do not learn what exists elsewhere. Responses carry can, the caller's permissions on each object, which the console uses to hide what would be refused.
  • Four-eyes approval. With KORTA_FOUR_EYES on (the default), applying a role is refused to its last editor and to the author of the plan. The administrator token may override this; the audit entry says so.
  • Secret scopes. A vault secret can be limited to instance labels (PUT /api/secrets/{name}/scope). Plans and applies refuse a role that uses it on any other instance, whoever runs them, so a staging role cannot copy a production password onto a staging host.
  • Links. Linking an application to a database needs deployment.credentials on that database, because the application receives its password.
  • Attribution. Operations record their actor; roles record who last changed them and plans who made them. The audit log adds sign-ins, user and token changes, credential reads, secret changes and deletions.
  • AI assistant. Claude sees only the instances and roles the user can read, and conversations belong to the user who started them.

Sign-in is a name and password, plus a code when the user has an authenticator app (TOTP, RFC 6238; each code works once). New users and resets get a one-time setup link valid for 72 hours, so owners never choose or see passwords. Sessions name the user and a credential epoch, which changes when the password, authenticator or account status changes, ending other sessions. Failed sign-ins are limited to 10 per client per 15 minutes.

Personal API tokens (korta_pat_…) act with their user's grants, expire after 90 days by default (at most 366), and stop working when revoked or when the user is disabled. The administrator token is the break-glass owner: it has every permission, has no account, and its console sign-ins are audited.

Not yet: single sign-on (OIDC) with group-to-grant mapping, and custom levels.

Security model

Secret Where it lives
Admin token KORTA_ADMIN_TOKEN or <data>/admin-token
User passwords Argon2id hashes (OWASP parameters), sealed in the state
Authenticator secrets State, sealed
API tokens, setup links Keyed hashes (Vault.MAC) in the state; the token itself is shown once
Vault key KORTA_VAULT_KEY or <data>/vault-key. Back it up separately from the data directory
SSH keys Vault, sealed per key ID
Role passwords, backup cipher, S3 secret, TLS keys Deployment secrets, sealed with the deployment ID and name as additional data
CA private key State, sealed

API responses list secret names only. Connection URLs, which contain a password, come only from GET /api/deployments/{id}/connection.

Roadmap

  1. Reverse proxy with TLS (Caddy or nginx) for frontends and public backends, with certificates from ACME or Korta's CA.
  2. CLI and MCP server on top of POST /api/provision, for one-command and one-prompt provisioning.
  3. Python applications on the same build-and-ship engine.
  4. PostgreSQL state backend for the manager, then optional high availability.
  5. Later: streaming replicas, off-host MySQL backups, host metrics, firewall rule removal, per-instance operation concurrency.

Known limitations

  • One operation runs at a time across the whole manager.
  • Firewall rules are added for access entries but not removed when entries are removed.
  • On RHEL, enabling CodeReady Builder needs an active subscription.
  • The PGDG apt signing key is downloaded over HTTPS and not pinned by fingerprint.
  • The manager does not lock its data directory; run one manager per directory.
  • Not yet verified on SELinux-enforcing hosts or with S3 repositories against real storage.
BUILT WITH KORTAAll documentation ↗