Install harnessd on Linux
Build harnessd, create its user and directories, write a minimal configuration, create the first admin, run it under systemd, and finish setup from the app.
This guide sets up harnessd on a dedicated Linux VM with Docker. The server listens on loopback only, and the Mac app reaches it through an SSH tunnel. It takes about 20 minutes. You need root on the VM, SSH access to it, and Docker to build.
Install#
- Build harnessd
On the server, from the SI Fabrik source, build in the official Swift image that matches the server's distribution. No Swift install is needed, and the binary uses the server's own system libraries:
Server Build image Ubuntu 24.04 swift:6.4-nobleUbuntu 22.04 swift:6.4-jammyAmazon Linux 2023, RHEL 9 (glibc 2.34) swift:6.4-rhel-ubi9Shellsudo docker run --rm -v "$PWD":/src -w /src swift:6.4-noble \ swift build -c release --product harnessd --static-swift-stdlibThe binary is
.build/release/harnessd. The image's glibc must not be newer than the server's: a binary built in a newer image won't start. Plainswift:6.4is Ubuntu 26.04, so its binaries won't start on older systems.On Amazon Linux 2023, install Docker and git first (the SSH user there is
ec2-user):Shellsudo dnf install -y docker git sudo systemctl enable --now docker - Create the user and directoriesShell
sudo useradd --system --home /var/lib/harness --shell /usr/sbin/nologin harness sudo install -m 0755 .build/release/harnessd /usr/local/bin/harnessd sudo install -d -o harness -g harness -m 0700 /etc/harness /var/lib/harness - Install Docker for the harness user
Agent sandboxes need Docker. Membership in the
dockergroup is root-equivalent, so prefer rootless Docker for theharnessuser (DOCKER_HOST=unix:///run/user/<uid>/docker.sock) and keep the VM dedicated to SI Fabrik. Installgit, andghif you want pull requests on GitHub. - Create the TLS certificate
A self-signed EC P-256 certificate is fine, because the app reaches the server through the tunnel and pins the certificate:
Shellsudo install -d -o harness -g harness -m 0700 /etc/harness/tls sudo openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 365 \ -subj "/CN=harnessd" -keyout /etc/harness/tls/privkey.pem -out /etc/harness/tls/fullchain.pem sudo chown harness:harness /etc/harness/tls/privkey.pem /etc/harness/tls/fullchain.pem sudo chmod 600 /etc/harness/tls/privkey.pemThe key must be owned by
harnesswith mode0600;harnessdrefuses to start otherwise. Renewing or replacing the certificate changes its fingerprint, and every user then has to update it in the app. See TLS and certificate pinning. - Write harnessd.yaml
Start from this minimal file, then see the full reference:
/etc/harness/harnessd.yamllisten: 127.0.0.1:8443 # loopback only; the app connects through an SSH tunnel. Never 0.0.0.0 tls: cert: /etc/harness/tls/fullchain.pem key: /etc/harness/tls/privkey.pem tools_listen: 172.17.0.1:7879 # the docker bridge data_dir: /var/lib/harness secrets_file: /etc/harness/secrets.env auth: require_totp_for_admins: true projects: - name: shop-storefront repo: git@github.com:your-org/shop-storefront.git workflow: WORKFLOW.md auto_start: trueWhy loopback: macOS rejects a self-signed certificate on any other address, even a pinned one. See Connect the app to a server.
- Write secrets.env/etc/harness/secrets.env
CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat… CLICKUP_API_TOKEN=pk_… HARNESS_GITHUB_TOKEN=github_pat_…Shellsudo chown harness:harness /etc/harness/secrets.env sudo chmod 600 /etc/harness/secrets.envSee Secrets for every name. You can also set the Claude token later from the app.
- Give the server repository access
For
repo:over SSH, add a deploy key for theharnessuser (read access to clone, write access to push task branches). Over https, setHARNESS_GITHUB_TOKENorHARNESS_GITLAB_TOKEN. - Create the first adminShell
sudo -u harness harnessd user add rob --role admin --config /etc/harness/harnessd.yaml sudo -u harness harnessd user totp rob --config /etc/harness/harnessd.yamlPasswords need at least 12 characters.
user totpprints anotpauth://URI and the secret for your authenticator app, then asks for the current code to confirm. - Check the configurationShell
sudo -u harness harnessd check --config /etc/harness/harnessd.yaml # ok: configuration passes security checks (1 project(s), 1 user(s))Fix every
error:line;warning:lines are advice. - Install the systemd unit
Copy the hardened unit below to
/etc/systemd/system/harnessd.service, then:Shellsudo systemctl daemon-reload sudo systemctl enable --now harnessd journalctl -u harnessd -fThe log prints the certificate fingerprint at startup (
certificate_sha256=…). You can also print it withsudo -u harness harnessd fingerprint --config /etc/harness/harnessd.yaml. - Connect from the Mac
Open an SSH tunnel and add the server in the app with the URL
https://127.0.0.1:9443and the fingerprint:Shellssh -N -i ~/.ssh/harness-server.pem -L 9443:127.0.0.1:8443 ubuntu@203.0.113.10See Connect the app to a server for the details and the firewall.
- Finish setup from the app
Signed in as the admin:
- Server Settings → Credentials: set the Claude token. Saving asks for an authenticator code.
- Sandboxes → the server → Agent Image → Build.
- If any project has
browser: enabled: true, buildharness-agent-browseron the same card. Tasks in browser projects fail without it. - If any project uses
sandbox.provider: cloudflare, set the bridge URL and token in Server Settings → Sandbox. Otherwise dispatch pauses withcloudflare_not_configured.
See Server Settings and Server sandboxes.
Workspaces on the server#
Leave workspace.root unset in the WORKFLOW.md of server projects. harnessd then puts task workspaces in <data_dir>/workspaces, for example /var/lib/harness/workspaces. A workspace.root copied from a Mac, such as a path under /private/tmp or ~/…, doesn't exist on the server, and the hardened unit can't write there anyway.
The systemd unit#
[Unit]
Description=SI Fabrik (Symphony orchestrator) server
After=network-online.target docker.service
Wants=network-online.target
[Service]
Type=simple
User=harness
Group=harness
ExecStartPre=/usr/local/bin/harnessd check --config /etc/harness/harnessd.yaml
ExecStart=/usr/local/bin/harnessd serve --config /etc/harness/harnessd.yaml
Restart=on-failure
RestartSec=5
UMask=0077
# Filesystem
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/harness
ReadOnlyPaths=/etc/harness
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
ProtectProc=invisible
# Privileges
NoNewPrivileges=true
CapabilityBoundingSet=
AmbientCapabilities=
RestrictSUIDSGID=true
LockPersonality=true
RestrictRealtime=true
RestrictNamespaces=true
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources @mount @swap @reboot @debug @obsolete @cpu-emulation
# Swift's dispatch worker threads set their own nice value (QoS); allow that one call from @resources.
SystemCallFilter=setpriority
# A blocked call fails with EPERM instead of killing the server.
SystemCallErrorNumber=EPERM
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
# Limits
LimitNOFILE=65536
TasksMax=4096
[Install]
WantedBy=multi-user.targetKeep the SystemCallFilter=setpriority and SystemCallErrorNumber=EPERM lines. Swift's dispatch threads call setpriority; without them the system-call filter kills the server with SIGSYS. A journal line like Failed to set thread priority for worker thread ... errno=13 is harmless.
ExecStartPre runs harnessd check before every start, so a configuration that fails the security checks never serves. /etc/harness is read-only to the service: the server never rewrites its configuration or secrets files.
Upgrade#
Build the new binary, install it over /usr/local/bin/harnessd, and sudo systemctl restart harnessd. Sessions are kept in memory only, so a restart signs everyone out and the Mac app asks for the password again.