Skip to content

SSH Installation

Minh Tu Le edited this page Aug 4, 2026 · 4 revisions

For the easiest setup, follow the SSH Quick Start Guide, which uses Terraform to provision the Gateway and an SSH server end to end. This guide is a manual alternative aimed at simple homelab setups, and at readers who want to see how the pieces fit together. It runs the Gateway as a standalone Docker container with Manual CA mode and configures an upstream SSH server to trust it. See SSH Overview for the underlying concepts.

Prerequisites

Before you begin, make sure you have:

  • A Twingate account with administrator privileges
  • A Linux host with Docker installed, reachable from the Connector on the Gateway's listen port (default 8443)
  • One or more SSH servers, each:
    • Reachable from the Gateway host on its SSH port
    • With a Linux user account that the Gateway will log in as (e.g. admin)
    • Where you can edit sshd_config and reload sshd (sudo access)
  • curl, jq, ssh-keygen, and openssl on the workstation running this guide

Installation

Step 1: Prepare your Twingate network

  • Log in to your Twingate Admin Console at https://<network-name>.twingate.com
  • Create a Remote Network that represents your homelab or environment:
    • Navigate to Network tab > Remote Networks and click the "+ Remote Network" button.
    • Take note of the Remote Network ID from the URL: https://<network-name>.twingate.com/networks/<remote-network-id>.
  • Create an API key with Read & Write permissions:
    • Settings > API (or navigate to https://<network-name>.twingate.com/settings/api)
    • Save the API key securely. You will not be able to see it again.

The remaining steps drive the Twingate API from the shell. Export these once and reuse them throughout the guide:

export TWINGATE_TENANT="<network-name>"
export TWINGATE_API_TOKEN="<api-token>"
export REMOTE_NETWORK_ID="<remote-network-id>"

Step 2: Install a Connector

  • Open the Remote Network you just created and click "Deploy Connector".
  • Choose the Docker option. The Admin Console generates a docker run command that includes the Connector tokens.
  • Run that command on any always-on host. Running it on the same host as the Gateway is fine.
  • Confirm the Connector is online in the Admin Console.

Tip

Skip this step if you already have a Connector online for this Remote Network.

Important

The Connector must be running version 1.82.0 or later. The Admin Console's "Deploy Connector" command pulls the latest image, so a fresh install is fine. If you're reusing an existing Connector, check its version on the Admin Console's Connectors page and update it if needed.

Step 3: Generate the SSH CA

ssh-keygen -t ed25519 -f ssh_ca -N "" -C "twingate-gateway-ca"

This produces two files:

  • ssh_ca, the private key used by the Gateway to sign user certificates.
  • ssh_ca.pub, the public key registered with Twingate in Step 5 and copied to every upstream SSH server in Step 9.

Step 4: Generate the X.509 CA

The certificate must include each SSH server's address and alias as Subject Alternative Names so the Twingate Client accepts the Gateway's TLS cert when connecting through any of them.

openssl ecparam -genkey -name prime256v1 -noout -out tls.key
openssl req -new -x509 -key tls.key -out tls.crt \
  -days 3650 -subj "/CN=Twingate Gateway TLS CA" \
  -addext "subjectAltName=DNS:home.int,IP:192.168.1.10"

For multiple upstreams, append more DNS: and IP: entries, e.g. -addext "subjectAltName=DNS:home.int,IP:192.168.1.10,DNS:nas.int,IP:192.168.1.11".

This produces tls.crt and tls.key. The Gateway uses the same self-signed certificate as both its TLS CA (registered with Twingate so the Client trusts the Gateway) and as the cert it serves on the outer TLS connection.

Step 5: Register the CAs and Gateway with Twingate

Three GraphQL mutations against https://${TWINGATE_TENANT}.twingate.com/api/graphql/. Run them in order. Each saves an ID into a shell variable that the next step uses.

5a. Register the SSH CA

SSH_CA_ID=$(curl -s -X POST \
  -H "X-API-KEY: ${TWINGATE_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg pk "$(cat ssh_ca.pub)" '{
    query: "mutation($name: String!, $publicKey: String!) {
      sshCertificateAuthorityCreate(name: $name, publicKey: $publicKey) {
        ok
        error
        entity { id }
      }
    }",
    variables: { name: "Homelab SSH CA", publicKey: $pk }
  }')" \
  "https://${TWINGATE_TENANT}.twingate.com/api/graphql/" \
  | jq -r '.data.sshCertificateAuthorityCreate.entity.id')

echo "SSH_CA_ID=$SSH_CA_ID"

5b. Register the X.509 CA

X509_CA_ID=$(curl -s -X POST \
  -H "X-API-KEY: ${TWINGATE_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg cert "$(cat tls.crt)" '{
    query: "mutation($name: String!, $certificate: String!) {
      x509CertificateAuthorityCreate(name: $name, certificate: $certificate) {
        ok
        error
        entity { id }
      }
    }",
    variables: { name: "Homelab X509 CA", certificate: $cert }
  }')" \
  "https://${TWINGATE_TENANT}.twingate.com/api/graphql/" \
  | jq -r '.data.x509CertificateAuthorityCreate.entity.id')

echo "X509_CA_ID=$X509_CA_ID"

5c. Create the Gateway

GATEWAY_ADDRESS is the IP:PORT where the Gateway will listen and where the Connector will reach it.

GATEWAY_ADDRESS="192.168.1.5:8443"

GATEWAY_ID=$(curl -s -X POST \
  -H "X-API-KEY: ${TWINGATE_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "$(jq -n \
        --arg address "$GATEWAY_ADDRESS" \
        --arg remoteNetworkId "$REMOTE_NETWORK_ID" \
        --arg x509CAId "$X509_CA_ID" \
        --arg sshCAId "$SSH_CA_ID" '{
    query: "mutation($address: String!, $remoteNetworkId: ID!, $x509CAId: ID!, $sshCAId: ID) {
      gatewayCreate(
        address: $address,
        remoteNetworkId: $remoteNetworkId,
        x509CAId: $x509CAId,
        sshCAId: $sshCAId
      ) {
        ok
        error
        entity { id }
      }
    }",
    variables: { address: $address, remoteNetworkId: $remoteNetworkId, x509CAId: $x509CAId, sshCAId: $sshCAId }
  }')" \
  "https://${TWINGATE_TENANT}.twingate.com/api/graphql/" \
  | jq -r '.data.gatewayCreate.entity.id')

echo "GATEWAY_ID=$GATEWAY_ID"

Step 6: Create an SSH resource

Run one mutation per SSH server. address is the upstream's IP or hostname without a port. alias is the friendly DNS name users will type to connect (e.g. ssh admin@home.int). Both must be present in the TLS cert SANs from Step 4.

RESOURCE_ID=$(curl -s -X POST \
  -H "X-API-KEY: ${TWINGATE_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "$(jq -n \
        --arg name "home-server" \
        --arg address "192.168.1.10" \
        --arg alias "home.int" \
        --arg remoteNetworkId "$REMOTE_NETWORK_ID" \
        --arg gatewayId "$GATEWAY_ID" '{
    query: "mutation($name: String!, $address: String!, $alias: String, $remoteNetworkId: ID!, $gatewayId: ID!) {
      sshResourceCreate(
        name: $name,
        address: $address,
        alias: $alias,
        remoteNetworkId: $remoteNetworkId,
        gatewayId: $gatewayId
      ) {
        ok
        error
        entity { id }
      }
    }",
    variables: { name: $name, address: $address, alias: $alias, remoteNetworkId: $remoteNetworkId, gatewayId: $gatewayId }
  }')" \
  "https://${TWINGATE_TENANT}.twingate.com/api/graphql/" \
  | jq -r '.data.sshResourceCreate.entity.id')

echo "RESOURCE_ID=$RESOURCE_ID"

After creating the resource, open the Admin Console and assign your user or group to it.

Step 7: Write the Gateway configuration

Create gateway-config.yaml. Replace <network-name> with your tenant name. Add one entry under ssh.upstreams for each SSH resource you created in Step 6.

twingate:
  network: <network-name>

port: 8443

tls:
  certificateFile: /etc/gateway/tls.crt
  privateKeyFile: /etc/gateway/tls.key

ssh:
  gateway:
    username: admin
  ca:
    manual:
      privateKeyFile: /etc/gateway/ssh_ca
  upstreams:
    - name: home-server
      address: 192.168.1.10:22

ssh.gateway.username is the Linux account the Gateway logs in as on every upstream. The upstream address includes the SSH port.

Step 8: Run the Gateway

Start the Gateway with Docker:

docker run -d --name twingate-gateway \
  -p 8443:8443 \
  -v $(pwd)/gateway-config.yaml:/etc/gateway/config.yaml:ro \
  -v $(pwd)/tls.crt:/etc/gateway/tls.crt:ro \
  -v $(pwd)/tls.key:/etc/gateway/tls.key:ro \
  -v $(pwd)/ssh_ca:/etc/gateway/ssh_ca:ro \
  twingate/gateway:latest \
  start --config /etc/gateway/config.yaml

Check the logs to confirm the Gateway started:

docker logs twingate-gateway

Tip

If you prefer not to use Docker, download the latest binary from GitHub Releases and run ./gateway start --config ./gateway-config.yaml. A small systemd unit can wrap this for unattended restart.

Step 9: Trust the CA on each upstream SSH server

On every server listed under ssh.upstreams, copy the Gateway's SSH CA public key and tell sshd to trust it:

sudo install -m 0644 ssh_ca.pub /etc/ssh/twingate_ca.pub
echo "TrustedUserCAKeys /etc/ssh/twingate_ca.pub" | \
  sudo tee /etc/ssh/sshd_config.d/twingate.conf
sudo sshd -t && sudo systemctl reload ssh

Important

The Linux user account matching ssh.gateway.username (admin in the example above) must already exist on the upstream server.

Step 10: Connect via the Gateway

  • In your Twingate Client, you should see the "home-server" resource. Hovering the resource shows a "Sync SSH Configuration" button.
  • Click the button. The Client adds @cert-authority entries for the Gateway to your ~/.ssh/known_hosts file.
  • Connect using the resource alias (or address) and the configured username:
ssh admin@home.int

No key or password is needed. The Gateway issues a short-lived SSH certificate signed by the SSH CA, and the upstream sshd trusts it via TrustedUserCAKeys.

Congratulations! You have successfully set up SSH access via the Gateway.

Hardening: upstream host certificates

By default, the Gateway uses Trust-On-First-Use (TOFU) to record each upstream SSH server's host key on the first connection. To eliminate TOFU, sign a host certificate with the same CA so the Gateway can verify the upstream's identity cryptographically.

On each upstream:

sudo ssh-keygen -s ssh_ca -I <hostname> -h \
  -n <hostname>,<address> -V +52w \
  /etc/ssh/ssh_host_ed25519_key.pub

Add the host key and certificate to /etc/ssh/sshd_config.d/twingate.conf:

HostKey /etc/ssh/ssh_host_ed25519_key
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub
TrustedUserCAKeys /etc/ssh/twingate_ca.pub

Reload sshd and the Gateway will verify the upstream host certificate against the CA on every connection.

Clone this wiki locally