Server (harnessd)

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#

  1. 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:

    ServerBuild image
    Ubuntu 24.04swift:6.4-noble
    Ubuntu 22.04swift:6.4-jammy
    Amazon Linux 2023, RHEL 9 (glibc 2.34)swift:6.4-rhel-ubi9
    Shell
    sudo docker run --rm -v "$PWD":/src -w /src swift:6.4-noble \
      swift build -c release --product harnessd --static-swift-stdlib

    The 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. Plain swift:6.4 is 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):

    Shell
    sudo dnf install -y docker git
    sudo systemctl enable --now docker
  2. Create the user and directories
    Shell
    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
  3. Install Docker for the harness user

    Agent sandboxes need Docker. Membership in the docker group is root-equivalent, so prefer rootless Docker for the harness user (DOCKER_HOST=unix:///run/user/<uid>/docker.sock) and keep the VM dedicated to SI Fabrik. Install git, and gh if you want pull requests on GitHub.

  4. 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:

    Shell
    sudo 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.pem

    The key must be owned by harness with mode 0600; harnessd refuses 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.

  5. Write harnessd.yaml

    Start from this minimal file, then see the full reference:

    /etc/harness/harnessd.yaml
    listen: 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: true

    Why loopback: macOS rejects a self-signed certificate on any other address, even a pinned one. See Connect the app to a server.

  6. Write secrets.env
    /etc/harness/secrets.env
    CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat…
    CLICKUP_API_TOKEN=pk_…
    HARNESS_GITHUB_TOKEN=github_pat_…
    Shell
    sudo chown harness:harness /etc/harness/secrets.env
    sudo chmod 600 /etc/harness/secrets.env

    See Secrets for every name. You can also set the Claude token later from the app.

  7. Give the server repository access

    For repo: over SSH, add a deploy key for the harness user (read access to clone, write access to push task branches). Over https, set HARNESS_GITHUB_TOKEN or HARNESS_GITLAB_TOKEN.

  8. Create the first admin
    Shell
    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.yaml

    Passwords need at least 12 characters. user totp prints an otpauth:// URI and the secret for your authenticator app, then asks for the current code to confirm.

  9. Check the configuration
    Shell
    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.

  10. Install the systemd unit

    Copy the hardened unit below to /etc/systemd/system/harnessd.service, then:

    Shell
    sudo systemctl daemon-reload
    sudo systemctl enable --now harnessd
    journalctl -u harnessd -f

    The log prints the certificate fingerprint at startup (certificate_sha256=…). You can also print it with sudo -u harness harnessd fingerprint --config /etc/harness/harnessd.yaml.

  11. Connect from the Mac

    Open an SSH tunnel and add the server in the app with the URL https://127.0.0.1:9443 and the fingerprint:

    Shell
    ssh -N -i ~/.ssh/harness-server.pem -L 9443:127.0.0.1:8443 ubuntu@203.0.113.10

    See Connect the app to a server for the details and the firewall.

  12. Finish setup from the app

    Signed in as the admin:

    1. Server Settings → Credentials: set the Claude token. Saving asks for an authenticator code.
    2. Sandboxes → the server → Agent Image → Build.
    3. If any project has browser: enabled: true, build harness-agent-browser on the same card. Tasks in browser projects fail without it.
    4. If any project uses sandbox.provider: cloudflare, set the bridge URL and token in Server Settings → Sandbox. Otherwise dispatch pauses with cloudflare_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#

/etc/systemd/system/harnessd.service
[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.target

Keep 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.

See also#