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:
| Setup | Certificate | In the app | Who it suits |
|---|---|---|---|
| SSH tunnel | Self-signed, pinned by its SHA-256 | Pinned fingerprint, URL https://127.0.0.1:9443 | A few admins who already have SSH access; nothing but SSH exposed |
| Load balancer | From a public authority, for a domain | Public certificate for the domain, URL https://fabrik.example.com | A 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
subjectAltNamefor the IP address andextendedKeyUsage=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.
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#
- Listen on loopback on the server/etc/harness/harnessd.yaml
listen: 127.0.0.1:8443 # loopback only; never 0.0.0.0Shellsudo systemctl restart harnessdNothing new is exposed: only SSH is reachable from outside.
- Get the fingerprintShell
sudo -u harness harnessd fingerprint --config /etc/harness/harnessd.yaml # 3F:A1:…:9CSend it to your users over a trusted channel.
- Open the tunnel on the MacShell
ssh -N -i ~/.ssh/harness-server.pem -L 9443:127.0.0.1:8443 ubuntu@203.0.113.10-Nforwards without running a remote command. Local port9443can be any free port. The SSH user isubuntuon Ubuntu images andec2-useron Amazon Linux.If the server listens on a private interface address instead, such as
listen: 10.0.1.20:8443, tunnel to that address:Shellssh -N -i ~/.ssh/harness-server.pem -L 9443:10.0.1.20:8443 ubuntu@203.0.113.10 - 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.
- URL:
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. Withlisten: 127.0.0.1:8443it isn't reachable from outside anyway. - If someone opens
8443regardless, 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.
- Let harnessd listen on the private address
The ALB reaches the instance on its private address, so
harnessdlistens there, and believesX-Forwarded-Foronly from the VPC:/etc/harness/harnessd.yamllisten: 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)Shellsudo systemctl restart harnessdKeep the self-signed certificate: it secures the hop from the ALB to the instance. See
trusted_proxiesfor why the range matters. - Request the certificate
In ACM, in the instance's region, request a public certificate for
fabrik.example.comwith DNS validation, and add the CNAME record it shows at your DNS provider. Wait until its status is Issued. - 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/0only 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
harnessdwithout going through the ALB.
- A new group for the ALB: inbound 443 from your users' addresses (your office or VPN range;
- Create the target group
Setting Value Target type Instances; register the server on port 8443 Protocol HTTPS, protocol version HTTP1 (terminals and live browsers use WebSockets) Health check HTTPS, path /healthz, success code200harnessdaccepts only TLS 1.3; the ALB uses it to targets when the listener has a TLS 1.3 security policy (next step). - Create the load balancer
Setting Value Scheme Internet-facing, IPv4 Subnets Two public subnets in different Availability Zones Security group The ALB's group only Listener HTTPS : 443 → the target group, with the ACM certificate Security policy ELBSecurityPolicy-TLS13-1-2-Res-2021-06Then, in the load balancer's Attributes:
- Idle timeout:
300seconds. Terminals and live browsers stay open while idle anyway:harnessdand 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).
- Idle timeout:
- Point the domain at the ALB
Create a CNAME (or a Route 53 alias) from
fabrik.example.comto the ALB's DNS name. Check it:Shellcurl https://fabrik.example.com/healthz # {"ok": true} - 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.
harnessdtakes the client's address fromX-Forwarded-Foronly when the connection comes fromtrusted_proxies, and then uses the rightmost address the proxies didn't add themselves. A client that sends its ownX-Forwarded-Forcan't choose its address, because the ALB appends the real one after it. Per-IP lockout and the audit log'sipthen 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.
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#
| Option | Status | Trade-offs |
|---|---|---|
| SSH tunnel | Works | Only 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) | Works | Needs 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 tested | Use 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 Tailscale | Works with a tunnel to loopback, or with a public certificate | Connecting 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.