CairnServer ← cairnserver.com

Documentation

Installing CairnServer, checking it, starting it, and the handful of behaviours that surprise people the first time.

Install

curl -fsSL https://cairnserver.com/install.sh | sudo bash

It fetches a release bundle, verifies its SHA-256, unpacks it and runs the installer inside. It refuses to run on anything but Linux with systemd, and tells you what it will do before it does it.

What it installs

Cairn manages Caddy, Postfix, PHP-FPM and podman, so it installs them — a box that cannot serve a site is not a server. acl comes too, because cairn-monitor needs it to watch config directories.

BIND is not installed. Most boxes do not serve DNS, and there is no Cairn worker managing it yet — a running BIND with no control plane over it is worse than none. --no-services skips the lot if you package your own.

Useful flags

--dry-run        show what would happen, change nothing
--version <v>    install a specific release
--no-services    do not apt-get Caddy, Postfix, PHP-FPM, podman
--verbose        list every step rather than collapsing the clean ones

Verify

sudo /usr/local/share/cairn/verify-install.sh

Run this before starting anything. The installer creates what the verifier asserts — they are a matched pair. If verify fails after a successful install, that is a bug in Cairn, not in your box.

It matters because the broker fails in confusing ways when its preconditions are missing: a wrong directory owner surfaces as PermissionDenied after authorisation has already logged allowed.

Start the services

Order matters. The broker first, then the workers, then the API.

sudo systemctl enable --now cairn-broker
sudo systemctl enable --now cairn-web cairn-mail cairn-php cairn-container cairn-fw
sudo systemctl enable --now cairn-api cairn-monitor

Nothing is started by the installer. Provisioning and starting are separate decisions, and the verifier exists to be run in between.

The bootstrap token

cairn-api mints an admin token the first time it starts and writes it to /etc/cairn/bootstrap-token (mode 0600).

sudo cat /etc/cairn/bootstrap-token

Use it once, to create a token scoped for whatever you are actually doing, then revoke it. It is the only credential that can mint others.

Scopes

Every request carries a bearer token, and every token carries scopes. A missing scope is a 403, which looks like a broken client if you have guessed the name — so here they are exactly.

ResourceScopes
Sitessites:read sites:write
Appsapps:read apps:write
PHP poolspools:read pools:write
Mail domainsmail_domains:read mail_domains:write mail_domains:delete
Firewallfirewall:read firewall:write
Tokenstokens:read tokens:create tokens:revoke

Note the shapes that differ: mail domains have a separate delete scope, and tokens use create and revoke rather than write.

Minting a token

curl -sX POST http://127.0.0.1:9100/v1/tokens \
  -H "Authorization: Bearer $(sudo cat /etc/cairn/bootstrap-token)" \
  -H 'Content-Type: application/json' \
  -d '{"name":"deploy","scopes":["sites:read","sites:write"]}'

The response contains raw_token. It is shown once.

The API

cairn-api listens on 127.0.0.1:9100 only. It is not exposed to the network, and should not be — reach it over an SSH tunnel, or from something running on the box.

RoutePurpose
/v1/sitesCaddy sites
/v1/appscontainers, as podman quadlets
/v1/poolsPHP-FPM pools
/v1/mailPostfix virtual domains
/v1/firewallthe nftables ruleset
/v1/tokensAPI tokens

There is no health endpoint. To check a box is reachable and your token works, ask it for one site — a box that answers TCP but rejects your token is not one you can manage.

curl -so /dev/null -w '%{http_code}\n' \
  -H "Authorization: Bearer $TOKEN" \
  'http://127.0.0.1:9100/v1/sites?limit=1'

Creating a site

curl -sX POST http://127.0.0.1:9100/v1/sites \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"blog","hostnames":["example.com"],"kind":"static"}'

kind is proxy, static or php. A static site serves from /srv/<name>; the path is derived from the name, never sent by the caller.

Only hostnames can be changed afterwards. A rename would point a live site at a socket nothing publishes, and the kind selects the renderer — Cairn treats both as a different site rather than an edit.

Firewall: confirm or revert

Every other resource can be got wrong and fixed by trying again. A firewall cannot — a rule that severs SSH removes the means of correcting it.

So an apply is provisional. PUT /v1/firewall loads the ruleset and arms a revert timer; you have two minutes to prove you can still reach the box by calling confirm. Miss it and the previous ruleset comes back on its own.

curl -sX POST http://127.0.0.1:9100/v1/firewall/confirm \
  -H "Authorization: Bearer $TOKEN"

Things that look like faults

404 from /v1/firewall

It means no ruleset has been written yet, which is true of every box that has just been installed. Not an error.

500 with ReloadFailed

The config was written but the service could not be reloaded — usually because that service is not installed. The database row is not created, deliberately: Cairn does not claim a site it could not bring up, so you are left with a file nothing is running rather than a record of something that does not work.

A worker cannot reach the broker

Every worker must be in the cairn-workers group, which is what opens /run/cairn/broker.sock. verify-install.sh checks this.

cairn-monitor is deliberately not in that group. It audits the broker, and a monitor that can ask the broker about the broker is asking the suspect to vouch for itself. It reads binaries through cairn-binaries instead.

BINARY INTEGRITY FAILURE at startup

cairn-monitor embeds a hash of every binary when it is built. If the binaries on disk were not built from the same tree, they will not match. Rebuild all of them together and redeploy.

PHP pools fail on Debian

Known. The broker's path table is compiled for PHP 8.3; Debian 13 ships 8.4. Sites and containers are unaffected.