← Back to posts
comparison guide self-hosted · · 12 min read

Self-Hosted SSH Bastion Host & Jump Server Guide: Teleport, Guacamole, Trisail 2026

Complete guide to self-hosted SSH bastion hosts and jump servers. Compare Teleport, Apache Guacamole, and Trisail with Docker Compose setups, RBAC configuration, and production best practices for 2026.

OS
Editorial Team

Why Self-Host an SSH Bastion Host?

Every homelab, small team, and distributed infrastructure faces the same problem: you have dozens of servers, VMs, and containers spread across clouds and local networks, and you need secure, audited access to all of them. Opening SSH port 22 on every machine is a security nightmare. Managing individual SSH keys across a growing fleet becomes unsustainable. And when someone leaves the team, you’re manually revoking keys on every server.

A bastion host (also called a jump server or SSH gateway) solves all of this. It’s a single hardened entry point that sits between you and your infrastructure. All SSH connections flow through it, giving you:

  • Centralized access control — one place to manage who can reach what
  • Session recording and auditing — full transcripts of every command executed
  • No direct SSH exposure — your backend servers never face the public internet
  • Role-based permissions — developers get different access than operators
  • Single sign-on — authenticate once, access everything

Commercial solutions like AWS Systems Manager Session Manager and ScaleFT exist, but they lock you into a vendor, charge per-node licensing fees, and send your session metadata to third-party servers. Self-hosted alternatives give you the same capabilities with full data sovereignty and zero per-node costs.


The Contenders: Teleport vs Guacamole vs Trisail

Three open-source projects dominate the self-hosted bastion space, each with a different philosophy.

Teleport (Gravitational)

Teleport is the most comprehensive option. It replaces SSH entirely with its own protocol built on top of SSH and TLS, adding identity-aware access, session recording, application proxying, kubernetes access, and database access — all through a single binary. It supports GitHub, OIDC, SAML, and local authentication.

Best for: Teams that want a unified access plane covering SSH, Kubernetes, databases, and web apps with strong audit requirements.

Apache Guacamole

Guacamole is a clientless remote desktop gateway. It supports SSH, RDP, VNC, and Kubernetes through a web browser — no client software needed. You connect to Guacamole’s web interface, select a connection, and get a terminal or desktop in your browser. It’s simpler than Teleport but covers more protocols.

Best for: Environments that need mixed SSH and remote desktop access through a single web portal, especially for less technical users.

Trisail (formerly ShellHub)

Trisail is a lightweight SSH gateway designed specifically for edge and IoT deployments. It uses a reverse-SSH model where agents on target machines initiate outbound connections to the gateway, meaning no firewall changes or port forwarding is required on the target side. It’s the simplest to deploy in network-restricted environments.

Best for: Homelab users and IoT/edge deployments where target machines are behind NAT or restrictive firewalls.


Feature Comparison

FeatureTeleportApache GuacamoleTrisail
SSH accessYes (native protocol)Yes (via web terminal)Yes (reverse SSH)
RDP / VNCNoYesNo
Kubernetes accessYes (kubectl proxy)Yes (web console)No
Database accessYes (PostgreSQL, MySQL, MongoDB)NoNo
Application proxyYes (HTTP/HTTPS)NoNo
AuthenticationOIDC, SAML, GitHub, localCAS, LDAP, SAML, Duo, TOTPGitHub, SAML, local
RBACFull policy engine (YAML)Connection-level permissionsOrganization-based
Session recordingYes (video + text)Yes (video)Yes (text only)
Audit logYes (structured events)Yes (connection logs)Yes (connection logs)
Hardware token (FIDO2)YesVia SSO providerNo
Access requestsYes (approval workflow)NoNo
Agent modelTeleport daemon on each nodeGuacamole daemon (guacd)Trisail agent on each node
Firewall requirementsOpen proxy port on Teleport serverOpen Guacamole web portOnly agents need outbound
Resource usageMedium (~256MB RAM)Low (~128MB RAM)Low (~64MB RAM)
LicenseOSS (AGPL-3.0) + EnterpriseApache 2.0Apache 2.0
docker supportOfficial imagesOfficial imagesOfficial images

Deployment Guide

1. Deploying Teleport

Teleport’s open-source edition (Community Edition) covers SSH access, session recording, RBAC, and OIDC authentication — everything most homelabs and small teams need.

Docker Compose Setup

Create docker-compose.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
version: "3.8"

services:
  teleport:
    image: public.ecr.aws/gravitational/teleport-distroless:16
    container_name: teleport
    restart: unless-stopped
    ports:
      - "3023:3023"   # SSH proxy
      - "3024:3024"   # Auth server (node heartbeats)
      - "3025:3025"   # Reverse tunnel
      - "443:443"     # Web UI
      - "3080:3080"   # HTTP (redirects to HTTPS)
    volumes:
      - ./config:/etc/teleport
      - ./data:/var/lib/teleport
    environment:
      - TELEPORT_AUTH_SERVER=localhost:3025
    command: ["teleport", "start", "-c", "/etc/teleport/teleport.yaml"]
    networks:
      - teleport-net

networks:
  teleport-net:
    driver: bridge

Teleport Configuration

Create config/teleport.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
version: v3
teleport:
  nodename: teleport-gateway
  data_dir: /var/lib/teleport
  log:
    output: stderr
    severity: INFO
    format:
      output: text

auth_service:
  enabled: "yes"
  listen_addr: 0.0.0.0:3025
  cluster_name: teleport.example.com
  authentication:
    type: local
    second_factor: on
    webauthn:
      rp_id: teleport.example.com
  # Default role for all users
  roles:
    - name: admin
      options:
        cert_format: standard
        max_session_ttl: 8h
        forward_agent: true
      allow:
        logins: ["root", "ubuntu", "deploy"]
        node_labels:
          "*": "*"

proxy_service:
  enabled: "yes"
  listen_addr: 0.0.0.0:3023
  web_listen_addr: 0.0.0.0:443
  public_addr: teleport.example.com:443
  ssh_public_addr: teleport.example.com:3023
  kube_listen_addr: 0.0.0.0:3026
  tunnel_listen_addr: 0.0.0.0:3024

ssh_service:
  enabled: "no"  # Teleport itself is the proxy, not a target node

Starting Teleport and Creating the First User

1
2
3
4
5
6
7
docker compose up -d

# Create the first admin user
docker exec teleport tctl users add admin --roles=editor,access \
  --logins=root,ubuntu,deploy

# This prints a URL — open it in your browser to complete setup

Connecting a Target Node

On each server you want to manage, install the Teleport agent and join it to the cluster:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# On the target node
docker run --rm --entrypoint=tctl \
  -v $(pwd)/config:/etc/teleport \
  public.ecr.aws/gravitational/teleport-distroless:16 \
  tokens add --type=node

# Copy the generated join token, then on the target:
docker run -d --name teleport-agent \
  --network host \
  --restart unless-stopped \
  -v /var/lib/teleport:/var/lib/teleport \
  public.ecr.aws/gravitational/teleport-distroless:16 \
  teleport start \
    --roles=node \
    --auth-server=teleport.example.com:3025 \
    --token=<JOIN_TOKEN> \
    --labels=env=production,team=backend

Creating RBAC Rules

Teleport’s role engine is its standout feature. Create role-dev.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
kind: role
version: v7
metadata:
  name: developer
spec:
  allow:
    logins: ["deploy", "app"]
    node_labels:
      "env": "staging"
      "team": ["backend", "frontend"]
    rules:
      - resources: ["session"]
        verbs: ["list", "read"]
  deny:
    node_labels:
      "env": "production"
    # Developers cannot access production nodes

Apply it:

1
tctl create -f role-dev.yaml

Now users with the developer role can SSH into staging nodes but are explicitly denied access to production — enforced at the protocol level, not just as a suggestion.


2. Deploying Apache Guacamole

Guacamole is ideal when you need both SSH terminal access and remote desktop (RDP/VNC) through a single web interface.

Docker Compose Setup

Guacamole requires three components: PostgreSQL (for connection storage), guacd (the daemon that handles protocols), and the Guacamole web application.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
version: "3.8"

services:
  guacd:
    image: guacamole/guacd:1.5.5
    container_name: guacd
    restart: unless-stopped
    networks:
      - guac-net

  postgres:
    image: postgres:16-alpine
    container_name: guac-postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: guacamole_db
      POSTGRES_USER: guacamole_user
      POSTGRES_PASSWORD: ${GUAC_DB_PASSWORD:-StrongPass123!}
    volumes:
      - guac-db-data:/var/lib/postgresql/data
    networks:
      - guac-net

  guacamole:
    image: guacamole/guacamole:1.5.5
    container_name: guacamole
    restart: unless-stopped
    depends_on:
      - guacd
      - postgres
    ports:
      - "8080:8080"
    environment:
      GUACD_HOSTNAME: guacd
      POSTGRES_HOSTNAME: postgres
      POSTGRES_DATABASE: guacamole_db
      POSTGRES_USER: guacamole_user
      POSTGRES_PASSWORD: ${GUAC_DB_PASSWORD:-StrongPass123!}
    networks:
      - guac-net

volumes:
  guac-db-data:

networks:
  guac-net:
    driver: bridge

Database Initialization

Guacamole’s Docker image includes a schema initialization script. Run it once before starting the service:

1
2
3
4
5
6
# Generate and apply the database schema
docker run --rm guacamole/guacamole:1.5.5 /opt/guacamole/bin/initdb.sh \
  --postgres > guac-init.sql

docker cp guac-init.sql guac-postgres:/tmp/guac-init.sql
docker exec guac-postgres psql -U guacamole_user -d guacamole_db -f /tmp/guac-init.sql

Then start everything:

1
docker compose up -d

Access Guacamole at http://your-server:8080/guacamole/. Default credentials are guacadmin / guacadmin — change this immediately.

Adding SSH Connections

Through the web interface:

  1. Log in as guacadmin
  2. Navigate to Settings → Connections → New Connection
  3. Configure the connection:
1
2
3
4
5
Name:        Web Server 01
Protocol:    SSH
Hostname:    192.168.1.50
Port:        22
Username:    ubuntu

Under Parameters → Authentication, select the password field or upload a private key for key-based authentication.

For parameterized connections that work across multiple hosts, use Connection Groups to organize servers by environment (staging, production, homelab) and apply connection-level permissions to user groups.


3. Deploying Trisail

Trisail’s reverse-SSH model is the simplest for homelab setups where target machines are behind NAT, CGNAT, or firewalls you cannot modify.

Docker Compose Setup

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
version: "3.8"

services:
  trisail-server:
    image: trisail/trisail:latest
    container_name: trisail-server
    restart: unless-stopped
    ports:
      - "8080:8080"   # Web UI
      - "443:443"     # HTTPS for agent connections
      - "2222:2222"   # SSH proxy port
    volumes:
      - ./trisail-data:/etc/trisail
      - ./certs:/etc/trisail/certs
    environment:
      TRISAIL_SERVER_KEY: ${TRISAIL_SERVER_KEY}
      TRISAIL_DOMAIN: trisail.example.com
    networks:
      - trisail-net

networks:
  trisail-net:
    driver: bridge

Installing the Agent on Target Nodes

The agent connects outbound to your Trisail server, so no inbound ports need to be opened on target machines:

1
2
3
curl -fsSL https://trisail.io/install.sh | sh -s -- \
  --server https://trisail.example.com \
  --key ${TRISAIL_SERVER_KEY}

Or via Docker on the target:

1
2
3
4
5
6
docker run -d --name trisail-agent \
  --restart unless-stopped \
  --network host \
  -e TRISAIL_SERVER=https://trisail.example.com \
  -e TRISAIL_KEY=${TRISAIL_SERVER_KEY} \
  trisail/trisail-agent:latest

The agent establishes a persistent reverse tunnel. You then connect through Trisail’s web UI or SSH proxy to reach any registered node — regardless of its network topology.


Choosing the Right Bastion Host

Choose Teleport when:

  • You need SSH, Kubernetes, database, and web app access through one gateway
  • Audit compliance requires detailed session recordings with search
  • You want approval-based access requests (a developer requests production access, an admin approves)
  • Your team uses OIDC/SAML identity providers and you want seamless SSO
  • You need hardware security key (FIDO2) enforcement for admin accounts

Choose Apache Guacamole when:

  • You need both SSH and remote desktop (RDP/VNC) access
  • Users should connect through a browser with no client installation
  • You have Windows servers alongside Linux machines
  • Your team includes non-technical users who need simple point-and-click access
  • You prefer Apache 2.0 licensing over AGPL

Choose Trisail when:

  • Target machines are behind NAT, firewalls, or CGNAT (common in residential ISPs)
  • You want the simplest possible deployment with minimal configuration
  • You’re managing IoT devices, edge servers, or homelab nodes
  • You need a lightweight solution with low resource overhead
  • Your primary use case is SSH access with basic session logging

Security Best Practices for Self-Hosted Bastion Hosts

Regardless of which solution you choose, follow these hardening steps:

1. Put the Bastion Behind a Reverse Proxy

Never expose nginxastion’s management port directly. Use Caddy or Nginx with TLS:

1
2
3
4
5
6
bastion.example.com {
    reverse_proxy localhost:8080
    tls {
        protocols tls1.2 tls1.3
    }
}

2. Enable Fail2Ban on the Bastion Host

1
2
3
4
5
6
# /etc/fail2ban/jail.local
[sshd]
enabled = true
maxretry = 3
bantime = 3600
findtime = 600

3. Restrict Bastion Host Access by IP

If your team works from known IP ranges, add a firewall rule:

1
2
3
4
# Allow only your office/home IPs to reach the bastion
ufw allow from 203.0.113.0/24 to any port 443
ufw allow from 198.51.100.0/24 to any port 443
ufw deny 443  # Deny all other access

4. Rotate Credentials Regularly

Set up a cron job to rotate join tokens and service account passwords:

1
2
# Teleport: rotate auth tokens monthly
0 3 1 * * /usr/local/bin/tctl tokens rotate --type=node

5. Ship Audit Logs to Long-Term Storage

Bastion hosts are prime targets — if an attacker compromises the gateway and deletes logs, you lose all forensic evidence. Forward logs to a separate system:

1
2
3
4
5
# Teleport audit log shipping (teleport.yaml)
auth_service:
  audit_events_uri:
    - file:///var/lib/teleport/audit
    - postgres://audit-user:pass@log-server:5432/audit_db

Migration from Direct SSH Access

If your servers currently allow direct SSH from the internet, migrate in phases:

  1. Phase 1: Deploy the bastion host and register all nodes as read-only (no access restrictions yet)
  2. Phase 2: Create user accounts and RBAC roles, test access through the bastion
  3. Phase 3: Remove direct SSH access from backend servers by updating firewall rules or sshd_config (AllowUsers directive to only accept connections from the bastion’s IP)
  4. Phase 4: Enable session recording and review the first week of logs to catch permission gaps
1
2
3
4
5
6
# On each backend server, restrict SSH to bastion IP only
# /etc/ssh/sshd_config
AllowUsers root ubuntu deploy
# Then in firewall:
iptables -A INPUT -p tcp --dport 22 -s <BASTION_IP> -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j DROP

Summary

Self-hosted SSH bastion hosts eliminate the friction of managing SSH keys across dozens of servers while adding security features most teams don’t even know they need — session recording, RBAC, and SSO. Teleport leads in features and compliance readiness, Guacamole wins for mixed SSH/desktop environments, and Trisail is the simplest choice for NAT-heavy deployments.

All three run on a single $5 VPS, support Docker Compose deployment, and cost nothing in licensing. The only investment is the initial setup time, which pays for itself the first time you need to audit who accessed what, or onboard a new team member in two clicks instead of distributing SSH keys to twelve servers.

Frequently Asked Questions (FAQ)

Which one should I choose in 2026?

The best choice depends on your specific requirements:

  • For beginners: Start with the simplest option that covers your core use case
  • For production: Choose the solution with the most active community and documentation
  • For teams: Look for collaboration features and user management
  • For privacy: Prefer fully open-source, self-hosted options with no telemetry

Refer to the comparison table above for detailed feature breakdowns.

Can I migrate between these tools?

Most tools support data import/export. Always:

  1. Backup your current data
  2. Test the migration on a staging environment
  3. Check official migration guides in the documentation

Are there free versions available?

All tools in this guide offer free, open-source editions. Some also provide paid plans with additional features, priority support, or managed hosting.

How do I get started?

  1. Review the comparison table to identify your requirements
  2. Visit the official documentation (links provided above)
  3. Start with a Docker Compose setup for easy testing
  4. Join the community forums for troubleshooting
Advertise here