Overview
Every homelab eventually accumulates a pile of SSH keys scattered across ~/.ssh/ — one per server, a few shared across machines, some with passwords you half-remember. That works until it doesn't: a compromised laptop, a forgotten authorized key on a decommissioned box, or an intern who left six months ago and still has authorized_keys entries everywhere.
Teleport solves this with a fundamentally different model. Instead of distributing static keys, it acts as a certificate authority. Users authenticate once (with MFA), receive a short-lived certificate (default: 12 hours), and that certificate is what gets them onto machines — no keys to manage, rotate, or forget. When the cert expires, access ends automatically.
This project walks you through deploying Teleport Community Edition on your homelab:
- Self-hosted Teleport cluster (Auth + Proxy + Web UI) via Docker Compose
- SSH node enrollment — bring existing Linux boxes under Teleport control
- MFA enforcement with TOTP
- Role-based access control: admin vs. read-only roles
- Session recording and audit log review
- Using
tshto SSH through the cluster
By the end you'll have a centralised access plane for every Linux server in your lab, with full session recordings you can replay, and a clean audit log of every login.
Architecture
Teleport has three logical components, all of which can run on a single host in a homelab:
┌─────────────────────────────────────────┐
│ Teleport Cluster Host │
│ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ Auth Service │ │ Proxy Service │ │
│ │ │ │ │ │
│ │ - CA (SSH, │ │ - Web UI :3080 │ │
│ │ TLS, DB) │ │ - SSH proxy │ │
│ │ - Audit log │ │ :3023 │ │
│ │ - User/Role │ │ - Reverse tunnel│ │
│ │ store │ │ :3024 │ │
│ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────┘
│
Certificate-based auth
│
┌───────────────┼───────────────┐
│ │ │
┌───────┐ ┌───────┐ ┌───────┐
│ Node1 │ │ Node2 │ │ Node3 │
│(SSH │ │(SSH │ │(SSH │
│agent) │ │agent) │ │agent) │
└───────┘ └───────┘ └───────┘
Auth Service — the brains. Maintains the certificate authority, stores users, roles, and audit events. Only the Proxy talks to Auth directly; nodes and clients never reach it.
Proxy Service — the front door. Handles the Web UI, client connections (port 3023), and reverse tunnels from nodes (port 3024). This is the only port you expose externally.
Node agents — Teleport binary running on each SSH host. Registers with the cluster via reverse tunnel, so nodes don't need inbound firewall rules.
tsh — the client CLI. Replaces ssh for cluster-managed connections.
Prerequisites
- A Linux host to run the Teleport cluster (Ubuntu 22.04+ or Debian 12+), 2 GB RAM minimum
- Docker and Docker Compose installed on the cluster host
- One or more additional Linux VMs/servers to enroll as nodes
- A hostname or IP for your Teleport cluster (a local hostname works fine for homelab)
- Ports 3023, 3024, 3025, and 3080 available on the cluster host
Step 1 — Deploy the Teleport Cluster with Docker Compose
Create a working directory and the Compose file:
mkdir -p ~/teleport/{data,config}
cd ~/teleportCreate docker-compose.yml:
services:
teleport:
image: public.ecr.aws/gravitational/teleport-ent-distroless:18
container_name: teleport
restart: unless-stopped
ports:
- "3023:3023" # SSH proxy
- "3024:3024" # Reverse tunnel
- "3025:3025" # Auth gRPC
- "3080:3080" # Web UI / HTTPS
volumes:
- ./data:/var/lib/teleport
- ./config:/etc/teleport
command: ["teleport", "start", "--config=/etc/teleport/teleport.yaml"]Generate the initial configuration file — replace teleport.homelab.local with your cluster hostname or IP:
docker run --rm \
-v $(pwd)/config:/etc/teleport \
public.ecr.aws/gravitational/teleport-ent-distroless:18 \
teleport configure \
--cluster-name=homelab \
--public-addr=teleport.homelab.local:3080 \
--output=/etc/teleport/teleport.yamlOpen config/teleport.yaml and make these changes under the auth_service section to enable local users and session recording:
auth_service:
enabled: true
cluster_name: homelab
# Store sessions locally (use S3 or GCS for production)
session_recording: node
authentication:
type: local
second_factor: otp # require TOTP MFA
webauthn:
rp_id: teleport.homelab.local
proxy_service:
enabled: true
web_listen_addr: "0.0.0.0:3080"
public_addr: "teleport.homelab.local:3080"
ssh_public_addr: "teleport.homelab.local:3023"
tunnel_public_addr: "teleport.homelab.local:3024"
https_keypairs: [] # Let Teleport generate a self-signed cert for homelab
ssh_service:
enabled: false # Don't run SSH service on the cluster host itselfStart the cluster:
docker compose up -d
docker compose logs -f teleportWait for the line Teleport is ready in the logs (usually 15–30 seconds).
Step 2 — Create the First Admin User
Exec into the container to use tctl:
docker exec teleport tctl users add admin \
--roles=editor,access \
--logins=root,ubuntu,debianThis outputs a one-time registration URL. Open it in a browser, set a password, and enroll your TOTP authenticator (Google Authenticator, Bitwarden, etc.).
Note on
--logins: This is the list of OS usernames the Teleport user is allowed to connect as. Add any login names present on your SSH nodes.
Verify the user was created:
docker exec teleport tctl users lsStep 3 — Install tsh on Your Workstation
Download the Teleport client for your workstation OS from the Teleport downloads page or via package manager:
# macOS
brew install teleport
# Ubuntu/Debian
curl https://goteleport.com/static/install.sh | bash -s 18
# Or download the binary directly
curl -L https://get.gravitational.com/teleport-v18-linux-amd64-bin.tar.gz | \
tar -xz -C /usr/local/bin teleport/tsh --strip-components=1Log in to your cluster (accept the self-signed cert warning for homelab):
tsh login --proxy=teleport.homelab.local:3080 --insecure --user=adminEnter your password, then your TOTP code. On success you'll see:
> Profile URL: https://teleport.homelab.local:3080
Logged in as: admin
Cluster: homelab
Roles: editor, access
Logins: root, ubuntu, debian
Kubernetes: disabled
Valid until: 2026-06-11 08:00:00 +0000 UTC [valid for 12h0m0s]
Extensions: permit-agent-forwarding, permit-port-forwarding, permit-pty
You now hold a short-lived SSH certificate valid for 12 hours. When it expires, you authenticate again — no persistent keys anywhere on your workstation.
Step 4 — Enroll Your First SSH Node
On the target Linux host (the machine you want to access via Teleport), install the Teleport agent:
# Replace with your actual version
curl -L https://get.gravitational.com/teleport-v18-linux-amd64-bin.tar.gz | \
tar -xz -C /usr/local/bin --strip-components=1
# Verify
teleport versionBack on the cluster host, generate a node join token (valid for 30 minutes):
docker exec teleport tctl tokens add \
--type=node \
--ttl=30mCopy the token value from the output. On the target SSH host, create /etc/teleport.yaml:
teleport:
nodename: node01
data_dir: /var/lib/teleport
auth_token: <YOUR_TOKEN_HERE>
proxy_server: teleport.homelab.local:3080
log:
output: stderr
severity: INFO
auth_service:
enabled: false
proxy_service:
enabled: false
ssh_service:
enabled: true
listen_addr: "0.0.0.0:3022"
labels:
env: homelab
role: ssh-node
# Forward commands for host inventory
commands:
- name: hostname
command: [hostname]
period: 1m0s
- name: arch
command: [uname, -p]
period: 1h0m0sCreate a systemd unit and start the agent:
cat > /etc/systemd/system/teleport.service << 'EOF'
[Unit]
Description=Teleport SSH Service
After=network.target
[Service]
Type=simple
Restart=on-failure
ExecStart=/usr/local/bin/teleport start --config=/etc/teleport.yaml
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now teleport
systemctl status teleportWithin a few seconds the node registers. Verify from the cluster:
docker exec teleport tctl nodes lsYou should see node01 appear with status Online.
Step 5 — SSH Through Teleport
From your workstation (where you ran tsh login), list available nodes:
tsh lsOutput:
Node Name Address Labels
--------- -------------- ---------------------------
node01 127.0.0.1:3022 arch=x86_64, env=homelab, hostname=node01
Connect to the node:
tsh ssh root@node01That's it — no key files, no host key prompts, no password. Teleport issued a certificate, the proxy routed the connection, and the node verified your cert against the cluster CA.
You can also use standard ssh with a generated config entry:
tsh config >> ~/.ssh/config
ssh root@node01.homelabStep 6 — Define Custom RBAC Roles
The built-in editor and access roles are broad. Create a tighter readonly role that can SSH but only as a non-root user, with no ability to forward ports:
Create role-readonly.yaml:
kind: role
version: v7
metadata:
name: readonly
spec:
allow:
logins: ['readonly-user']
node_labels:
env: homelab
rules:
- resources: ['session']
verbs: ['list', 'read']
deny:
logins: ['root']
options:
forward_agent: false
port_forwarding: false
max_session_ttl: 4h
enhanced_recording:
- command
- networkApply it:
docker exec -i teleport tctl create -f < role-readonly.yamlCreate a restricted user:
docker exec teleport tctl users add readonly-user \
--roles=readonly \
--logins=readonly-userThis user can reach homelab nodes but cannot escalate to root, forward ports, or modify cluster resources.
Step 7 — Add a Second Admin with Separate MFA
For a multi-person homelab or to test role isolation, add a second user:
docker exec teleport tctl users add labuser \
--roles=access \
--logins=ubuntu,labuserWalk through the same TOTP enrollment. Now both users must authenticate independently — no shared credentials.
Step 8 — Review Session Recordings and Audit Log
Every tsh ssh session is recorded as an interactive terminal stream (with timing data for playback).
List recorded sessions:
docker exec teleport tctl recordings lsPlay back a session by ID in the terminal:
tsh play <session-id>The Web UI at https://teleport.homelab.local:3080 also has a full session player — you can watch the terminal replay at real speed, seek through it, and see the exact commands typed.
View the raw audit log:
docker exec teleport tctl audit events --format=json | jq .Key event types to watch for:
| Event | Meaning |
|---|---|
user.login | Successful web/tsh login |
session.start | SSH session opened |
session.end | SSH session closed (with recording reference) |
auth | Authentication attempt (success or failure) |
user.create | New user added |
role.created | Role definition changed |
Export events for a time range (useful for weekly review):
docker exec teleport tctl audit events \
--from="2026-06-01" \
--to="2026-06-10" \
--format=json > audit-june.jsonTesting
Run these checks to validate your deployment:
# 1. Verify cluster health
docker exec teleport tctl status
# 2. List all registered nodes
docker exec teleport tctl nodes ls
# 3. List all users and their roles
docker exec teleport tctl users ls
# 4. SSH to a node and confirm session recording starts
tsh ssh ubuntu@node01 -- echo "Teleport recording test"
# 5. Confirm the session appears in recordings
docker exec teleport tctl recordings ls | head -5
# 6. Test role enforcement — readonly-user cannot sudo to root
tsh ssh --user=readonly-user readonly-user@node01 -- sudo whoami
# Expected: sudo: Permission denied / role denies root login
# 7. Confirm expired cert is rejected (after manually expiring)
# tsh logout && tsh ssh root@node01 → should prompt for re-authTroubleshooting
Node won't join / stays Offline
Check the agent logs on the node: journalctl -u teleport -f. Common cause: the cluster hostname isn't resolving from the node. Add an /etc/hosts entry pointing teleport.homelab.local at your cluster host IP.
Browser shows untrusted certificate
Expected for a self-signed homelab cert. In Firefox: Advanced → Accept Risk. For a trusted cert, add your cluster to your local CA (or use a real domain with Let's Encrypt — Teleport supports ACME natively).
tsh login fails with "dial tcp: connection refused"
Verify ports 3023/3080 are exposed from the container: docker compose ps should show them mapped. Check your firewall: ufw allow 3023/tcp && ufw allow 3080/tcp.
tctl commands return permission errors
You must exec into the container as root: docker exec -u root teleport tctl <cmd>. The default container entrypoint drops privileges.
Deployment Notes
For a production-style homelab deployment:
Persistent data: The ./data volume in Docker Compose stores all cluster state (audit log, session recordings, CA keys). Back it up regularly — losing it means re-issuing all join tokens and certificates.
TLS with a real cert: If you have a domain, Teleport supports ACME out of the box:
proxy_service:
acme:
enabled: true
email: admin@yourdomain.comFirewall rules: Only port 3080 needs to be externally reachable if you're using reverse tunnels. Nodes connect outbound to 3024; they don't need inbound holes.
Upgrades: Pull the new image tag and docker compose up -d --pull always. Teleport maintains backward compatibility across minor versions. For major version upgrades (e.g., v17 → v18), read the upgrade guide — auth and proxy must be upgraded before nodes.
Extensions and Next Steps
Once the core SSH PAM is running, Teleport's Community Edition covers several more access planes:
Kubernetes Access — enroll your k3s or k8s cluster so kubectl goes through Teleport with the same cert-based auth and session recording:
teleport configure kube --cluster-name=homelab-k8s --proxy=teleport.homelab.local:3080Database Access — proxy connections to PostgreSQL, MySQL, or MongoDB through Teleport, with the same audit trail and no static credentials stored in apps.
Application Access — put internal web apps (Grafana, Portainer, etc.) behind Teleport's app proxy for SSO instead of a VPN.
GitHub/OIDC SSO — replace local passwords with GitHub OAuth or any OIDC provider (Authentik, Keycloak). Users authenticate with their existing identity; Teleport maps groups to roles.
# Add to auth_service in teleport.yaml
authentication:
type: github
github:
client_id: <YOUR_GITHUB_CLIENT_ID>
client_secret: <YOUR_GITHUB_CLIENT_SECRET>
redirect_url: https://teleport.homelab.local:3080/v1/webapi/github/callback
display: GitHub
teams_to_roles:
- organization: your-org
team: homelab-admins
roles: [editor, access]Ansible/Terraform integration — use tsh as the SSH binary in Ansible inventory for certificate-backed automation, no static keys needed in CI/CD.
Hardware MFA — swap TOTP for WebAuthn with a YubiKey or platform authenticator for phishing-resistant MFA on every SSH session.
Summary
You now have a fully functional zero-trust SSH access platform running in your homelab:
- No more static keys: short-lived certificates replace
~/.ssh/authorized_keysentries across all enrolled nodes - MFA on every login: TOTP challenge before any certificate is issued
- Full session recording: every terminal session is captured and replayable from the Web UI
- Clean audit log: every login, session start/end, and admin action is logged with user, IP, and timestamp
- Centralised RBAC: add or revoke access by editing a user record or role — no manual key cleanup across dozens of servers
The same Teleport cluster you built here scales to dozens of nodes, and the Community Edition is free and open-source. When your lab grows beyond SSH, the Kubernetes, database, and application access features slot in without rebuilding anything.