PROJECTIntermediate

Teleport PAM: Zero-Trust Privileged Access for Your Homelab

Deploy Teleport's open-source privileged access management platform to replace static SSH keys with short-lived certificates, enforce MFA, record every...

Dylan H.

Projects

June 10, 2026
11 min read
3-5 hours
Teleport PAM: Zero-Trust Privileged Access for Your Homelab

Tools & Technologies

Teleport v18Docker Composetctl (Teleport CLI)tsh (Teleport SSH client)OpenSSH

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 tsh to 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 ~/teleport

Create 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.yaml

Open 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 itself

Start the cluster:

docker compose up -d
docker compose logs -f teleport

Wait 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,debian

This 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 ls

Step 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=1

Log in to your cluster (accept the self-signed cert warning for homelab):

tsh login --proxy=teleport.homelab.local:3080 --insecure --user=admin

Enter 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 version

Back on the cluster host, generate a node join token (valid for 30 minutes):

docker exec teleport tctl tokens add \
  --type=node \
  --ttl=30m

Copy 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: 1h0m0s

Create 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 teleport

Within a few seconds the node registers. Verify from the cluster:

docker exec teleport tctl nodes ls

You should see node01 appear with status Online.


Step 5 — SSH Through Teleport

From your workstation (where you ran tsh login), list available nodes:

tsh ls

Output:

Node Name  Address         Labels
---------  --------------  ---------------------------
node01     127.0.0.1:3022  arch=x86_64, env=homelab, hostname=node01

Connect to the node:

tsh ssh root@node01

That'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.homelab

Step 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
      - network

Apply it:

docker exec -i teleport tctl create -f < role-readonly.yaml

Create a restricted user:

docker exec teleport tctl users add readonly-user \
  --roles=readonly \
  --logins=readonly-user

This 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,labuser

Walk 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 ls

Play 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:

EventMeaning
user.loginSuccessful web/tsh login
session.startSSH session opened
session.endSSH session closed (with recording reference)
authAuthentication attempt (success or failure)
user.createNew user added
role.createdRole 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.json

Testing

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-auth

Troubleshooting

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.com

Firewall 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:3080

Database 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_keys entries 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.