Server (harnessd)

Connect the app to a server

Reach harnessd from the Mac app through an SSH tunnel with a pinned self-signed certificate, or through a load balancer with a public certificate for a domain.

The Mac app talks to harnessd over HTTPS. There are two supported setups:

SetupCertificateIn the appWho it suits
SSH tunnelSelf-signed, pinned by its SHA-256Pinned fingerprint, URL https://127.0.0.1:9443A few admins who already have SSH access; nothing but SSH exposed
Load balancerFrom a public authority, for a domainPublic certificate for the domain, URL https://fabrik.example.comA team; no SSH access or tunnels for users

The rest of this page explains why a self-signed certificate needs the tunnel, then sets up each option.

Why a tunnel#

macOS App Transport Security (ATS) rejects a self-signed certificate on any address other than loopback, before the app's own pinning check runs. Connecting straight to https://203.0.113.10:8443 or https://10.0.1.20:8443 fails with A TLS error caused the secure connection to fail (-1200 / -9802). This happens even when:

  • the certificate has a subjectAltName for the IP address and extendedKeyUsage=serverAuth;
  • the app pins the certificate's SHA-256 fingerprint and anchors trust to it.

ATS exceptions can't name IP addresses, so there's no per-server opt-out. Loopback (127.0.0.1, localhost) is exempt, so a tunnel that ends on 127.0.0.1 on the Mac works.

A VPN address on its own doesn't work

Listening on a VPN or private address and connecting to it directly fails the same way. Use the VPN to reach the server's SSH port, and tunnel to loopback from there.

Set up the SSH tunnel#

  1. Listen on loopback on the server
    /etc/harness/harnessd.yaml
    listen: 127.0.0.1:8443        # loopback only; never 0.0.0.0
    Shell
    sudo systemctl restart harnessd

    Nothing new is exposed: only SSH is reachable from outside.

  2. Get the fingerprint
    Shell
    sudo -u harness harnessd fingerprint --config /etc/harness/harnessd.yaml
    # 3F:A1:…:9C

    Send it to your users over a trusted channel.

  3. Open the tunnel on the Mac
    Shell
    ssh -N -i ~/.ssh/harness-server.pem -L 9443:127.0.0.1:8443 ubuntu@203.0.113.10

    -N forwards without running a remote command. Local port 9443 can be any free port. The SSH user is ubuntu on Ubuntu images and ec2-user on Amazon Linux.

    If the server listens on a private interface address instead, such as listen: 10.0.1.20:8443, tunnel to that address:

    Shell
    ssh -N -i ~/.ssh/harness-server.pem -L 9443:10.0.1.20:8443 ubuntu@203.0.113.10
  4. Add the server in the app

    In Add Server, use:

    • URL: https://127.0.0.1:9443
    • Certificate SHA-256: the fingerprint from the server.

    Then sign in. See Connect to a server.

Keep the tunnel running while you use the server; if it drops, the app can't reach the server until you open it again. Each person who uses the app opens their own tunnel, so each needs SSH access to the server.

Restarting harnessd signs everyone out, because sessions are kept in memory only. Users sign in again; the tunnel itself doesn't need restarting.

Firewall and security group#

  • Open only SSH (22), and restrict its source to the admins' addresses (<your-ip>/32), or to your VPN.
  • Don't open 8443. With listen: 127.0.0.1:8443 it isn't reachable from outside anyway.
  • If someone opens 8443 regardless, restrict its source to specific IPs. The app still can't use it directly with a self-signed certificate.

Behind a load balancer#

An AWS Application Load Balancer (ALB) holds a certificate from AWS Certificate Manager (ACM) for your domain and forwards to harnessd over TLS. macOS trusts the ACM certificate, so no tunnel is needed, and ACM renews it without anything changing in the app. These steps use fabrik.example.com; any domain you control works.

Use an Application Load Balancer. A Network Load Balancer that passes TLS through would show the app harnessd's self-signed certificate, which macOS rejects, and a Gateway Load Balancer is for network appliances.

  1. Let harnessd listen on the private address

    The ALB reaches the instance on its private address, so harnessd listens there, and believes X-Forwarded-For only from the VPC:

    /etc/harness/harnessd.yaml
    listen: 10.0.1.20:8443            # the instance's private address
    trusted_proxies: [10.0.0.0/16]    # the VPC (or just the ALB's subnets)
    Shell
    sudo systemctl restart harnessd

    Keep the self-signed certificate: it secures the hop from the ALB to the instance. See trusted_proxies for why the range matters.

  2. Request the certificate

    In ACM, in the instance's region, request a public certificate for fabrik.example.com with DNS validation, and add the CNAME record it shows at your DNS provider. Wait until its status is Issued.

  3. Create the security groups
    • A new group for the ALB: inbound 443 from your users' addresses (your office or VPN range; 0.0.0.0/0 only if they really connect from anywhere).
    • The instance's group: inbound 8443 only from the ALB's security group, and 22 only from admins. Remove any other rule for 8443, so nobody can reach harnessd without going through the ALB.
  4. Create the target group
    SettingValue
    Target typeInstances; register the server on port 8443
    ProtocolHTTPS, protocol version HTTP1 (terminals and live browsers use WebSockets)
    Health checkHTTPS, path /healthz, success code 200

    harnessd accepts only TLS 1.3; the ALB uses it to targets when the listener has a TLS 1.3 security policy (next step).

  5. Create the load balancer
    SettingValue
    SchemeInternet-facing, IPv4
    SubnetsTwo public subnets in different Availability Zones
    Security groupThe ALB's group only
    ListenerHTTPS : 443 → the target group, with the ACM certificate
    Security policyELBSecurityPolicy-TLS13-1-2-Res-2021-06

    Then, in the load balancer's Attributes:

    • Idle timeout: 300 seconds. Terminals and live browsers stay open while idle anyway: harnessd and the app each send a WebSocket ping every 20 seconds.
    • Desync mitigation mode: Strictest.
    • Drop invalid header fields: On.
    • X-Forwarded-For header: Append (the default).
  6. Point the domain at the ALB

    Create a CNAME (or a Route 53 alias) from fabrik.example.com to the ALB's DNS name. Check it:

    Shell
    curl https://fabrik.example.com/healthz
    # {"ok": true}
  7. Add the server in the app

    In Add Server, set Certificate to Public certificate for the domain, and URL to https://fabrik.example.com. There's no fingerprint to enter. See Connect to a server.

How the ALB setup stays secure#

  • Certificate. macOS checks the certificate against the public authorities it ships with (not roots added to the Mac), for the exact domain in the URL, and requires Certificate Transparency. The app refuses an IP address or a name without a dot in this mode.
  • Client addresses. harnessd takes the client's address from X-Forwarded-For only when the connection comes from trusted_proxies, and then uses the rightmost address the proxies didn't add themselves. A client that sends its own X-Forwarded-For can't choose its address, because the ALB appends the real one after it. Per-IP lockout and the audit log's ip then show each user's real address.
  • No way around the ALB. With 8443 open only to the ALB's security group, every request passes the ALB's TLS policy and header checks.
  • Optional: attach AWS WAF to the ALB, with a rate-based rule on /api/v1/login, and turn on ALB access logs.
Keep trusted_proxies narrow

Any machine inside trusted_proxies can name any client address. List the VPC or the ALB's subnets, never a public range; harnessd check refuses ranges wider than /8 (IPv4) or /32 (IPv6). An SSH tunnel to the instance's private address also comes from inside the range, so people with SSH access can set their own address in the audit log. They have shell access to the server anyway.

Other ways to connect#

OptionStatusTrade-offs
SSH tunnelWorksOnly SSH is exposed. Each user needs SSH access and keeps a tunnel open. Every connection reaches harnessd from the server itself, so per-IP lockout and the audit log's ip don't tell users apart; per-account lockout still applies.
Load balancer (AWS ALB with an ACM certificate)WorksNeeds a domain. Users need no SSH access. Per-IP lockout and the audit log see real addresses through trusted_proxies.
Public certificate on harnessd itself (for example Let's Encrypt for a domain)Supported, not yet testedUse Public certificate for the domain in the app. You renew the certificate on the server and restart harnessd; open 443 or 8443 only to your users.
VPN or TailscaleWorks with a tunnel to loopback, or with a public certificateConnecting to a VPN address with a self-signed certificate hits the ATS rule above.

When the certificate changes#

With a public certificate, nothing changes in the app when it renews. With a pinned certificate, renewing or replacing it changes its fingerprint. Each user removes the server in the app and adds it again with the new fingerprint. See Rotate the certificate.

See also#