Skip to content

Jump Server (Reverse SSH)

Use reverse SSH tunnels and a jump server to allow internal network users to connect to work servers via a public relay server — no VPN required, with improved stability.

Motivation: Optimize connection stability and bypass VPN requirements.

Architecture Overview

User PC                     Jump Server (Public)            Work Server (Internal)
                              ┌─────────────────┐
zhangsan ──── ssh ────> :10001│                 │
lisi     ──── ssh ────> :10002│  Reverse Tunnel │──── autossh ────> localhost:22
wangwu   ──── ssh ────> :10003│                 │
                              └─────────────────┘

Core design:

  • systemd template instances tunnel@port.service — one independent process per port
  • A single port failure does not affect other users
  • Adding/removing users = starting/stopping systemd instances, no config file edits needed
Location Role Description
Jump server Dedicated tunnel user, pipe only One-time setup
Work server systemd template + autossh, one instance per port User changes happen here
User PC Each connects to a different port Only needs SSH config

Key & Authentication Design

Three types of keys, each with a distinct purpose:

Key Holder Purpose
Tunnel key tunnel_key Work server autossh authenticates to jump server, establishes reverse tunnel
Management key management_key Work server Remotely manage jump server — push/delete user public keys
User key × N Each user Log into work server + ProxyJump through jump server

The distribution of user public keys is the core of the design — the same public key is written to both servers, serving different purposes:

User provides public key to admin
    add-user.sh
        ├──> Work server: ~username/.ssh/authorized_keys
        │    (for SSH login)
        └──> Jump server: /home/tunnel/.ssh/authorized_keys
             with restrict,port-forwarding,permitopen="localhost:PORT"
             (restricted to forwarding only to the user's tunnel port)

Authentication flow when a user connects:

  1. SSH client authenticates to jump server's tunnel user using the user's private key (port-forwarding only)
  2. Through the tunnel, reaches work server's port 22
  3. SSH client authenticates to work server's user account using the same private key

The same user key pair handles both authentication steps.

Prerequisites

  • A server with a public IP to act as the jump server (e.g., a cloud VPS)
  • Work server must be able to SSH to the jump server
  • Admin needs root access on both servers

Deployment Steps

Step 1: Jump Server — One-time Setup

SSH into the jump server and run:

# Create tunnel user (no shell login)
sudo useradd -m -s /usr/sbin/nologin tunnel
sudo mkdir -p /home/tunnel/.ssh
sudo chmod 700 /home/tunnel/.ssh

If using direct-connect mode (not ProxyJump), also add to /etc/ssh/sshd_config:

GatewayPorts clientspecified

Then restart sshd: sudo systemctl restart sshd

ProxyJump mode (recommended)

In ProxyJump mode, tunnel ports are bound to the jump server's loopback interface only — not exposed to the public internet. This is more secure and recommended for most scenarios.

Step 2: Work Server — Installation

  1. Copy the script directory (containing setup.sh, tunnel.conf, tunnel@.service, etc.) to the work server

  2. Edit tunnel.conf with your jump server details:

    # tunnel.conf — environment variables for systemd template instances
    RELAY_HOST=203.0.113.50    # Jump server IP or domain
    RELAY_PORT=22               # Jump server SSH port
    RELAY_USER=tunnel           # Tunnel user on jump server
    
    # Reverse tunnel bind address — leave empty for ProxyJump mode
    BIND_PREFIX=
    
    # Management key (for pushing/removing user public keys to jump server)
    RELAY_MGMT_USER=root
    MGMT_KEY=/etc/autossh/management_key
    
  3. Run the setup script:

    sudo ./setup.sh
    

    The script automatically:

    • Creates the tunnel-runner system user (minimum privilege)
    • Generates tunnel and management keys in /etc/autossh/
    • Installs the systemd template to /etc/systemd/system/
    • Fetches the jump server's host key
    • Creates the port registry
    • Starts the initial tunnel instance
  4. Follow the prompts from the setup script to configure keys on the jump server:

    # On the jump server:
    
    # 1. Write the tunnel public key (restrict to port-forwarding only)
    echo 'restrict,port-forwarding,permitopen="localhost:22" ssh-ed25519 AAAA... tunnel-main' | \
        sudo tee /home/tunnel/.ssh/authorized_keys
    
    # 2. Add the management public key to root's authorized_keys
    echo 'ssh-ed25519 AAAA... tunnel-management' | \
        sudo tee -a /root/.ssh/authorized_keys
    
    # 3. Set permissions
    sudo chown -R tunnel:tunnel /home/tunnel/.ssh
    sudo chmod 600 /home/tunnel/.ssh/authorized_keys
    

Step 3: Verification

# On the work server, check tunnel status
systemctl status tunnel@10001

# List all tunnel instances
systemctl list-units 'tunnel@*'

# View port registry
cat /etc/autossh/port-registry.txt

# Test connection from outside
ssh my-server

User Management

Adding a User

sudo ./add-user.sh zhangsan \
    --key-file /tmp/zhangsan.pub \
    --port 10001 \
    --remark "Researcher"

The script automatically:

  1. Creates a system user on the work server (if it doesn't exist)
  2. Writes the user's public key to ~/.ssh/authorized_keys
  3. Pushes the key to the jump server (with restrict,port-forwarding,permitopen restrictions)
  4. Updates the port registry /etc/autossh/port-registry.txt
  5. Starts and enables the systemd tunnel instance

After completion, send this SSH config to the user (replace with actual values):

Host my-server
    HostName localhost
    Port 10001
    ProxyJump tunnel@203.0.113.50
    User zhangsan
    ServerAliveInterval 30

You can also pass the public key inline:

sudo ./add-user.sh lisi \
    --key "ssh-ed25519 AAAA... lisi@laptop" \
    --port 10002 \
    --remark "RA"

Removing a User

# Keep user home directory and data
sudo ./remove-user.sh zhangsan

# Also delete user home directory
sudo ./remove-user.sh zhangsan --delete-home

The script will:

  1. Stop and disable the tunnel instance for that port
  2. Remove the corresponding authorized_keys entry from the jump server (matches permitopen port)
  3. Remove the record from the port registry

Pushing / Updating Keys

If you didn't provide a key when adding a user, or need to update it:

sudo ./push-key.sh zhangsan --key-file /tmp/zhangsan_new.pub
# or
sudo ./push-key.sh zhangsan --key "ssh-ed25519 AAAA... zhangsan@new-laptop"

systemd Template Management

Tunnels use systemd template instances:

# /etc/systemd/system/tunnel@.service
[Unit]
Description=Reverse SSH Tunnel - port %i
After=network-online.target ssh.service
Wants=network-online.target

[Service]
Type=simple
User=tunnel-runner
EnvironmentFile=/etc/autossh/tunnel.conf
ExecStart=/usr/bin/autossh -M 0 -N \
    -R %i:localhost:22 \
    -o "ServerAliveInterval=30" \
    -o "ServerAliveCountMax=3" \
    -o "Port=${RELAY_PORT}" \
    -o "StrictHostKeyChecking=yes" \
    -o "UserKnownHostsFile=/etc/autossh/known_hosts" \
    -o "ExitOnForwardFailure=yes" \
    -o "IdentitiesOnly=yes" \
    -i /etc/autossh/tunnel_key \
    ${RELAY_USER}@${RELAY_HOST}
Restart=always
RestartSec=10
StartLimitIntervalSec=300
StartLimitBurst=5

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/etc/autossh
RuntimeDirectory=tunnel-%i

SyslogIdentifier=tunnel@%i
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Common operations:

systemctl status tunnel@10001       # Check single tunnel status
systemctl restart tunnel@10001      # Restart a tunnel
systemctl stop tunnel@10001         # Stop
systemctl list-units 'tunnel@*'     # List all instances
journalctl -u tunnel@10001 -f       # Follow logs in real-time

Health Monitoring

Set up a cron job to periodically check tunnel health:

# Check every 5 minutes
*/5 * * * * /opt/tunnel/health-check.sh

The health check script iterates through all ports in the registry, checks systemd service status, and logs failures to syslog (tagged tunnel-health).

View health logs:

grep "tunnel-health" /var/log/syslog

Port Registry

Location: /etc/autossh/port-registry.txt

# Port  Username  Created     Note
10001   zhangsan  2026-01-15  Admin
10002   lisi      2026-01-16  Researcher
10003   wangwu    2026-01-20  Visiting scholar

Security Measures

Measure Description
systemd template instances Independent process per port — single failure doesn't affect others
Dedicated tunnel-runner user Does not run as root — least privilege principle
restrict + permitopen Jump server limits tunnel user to port-forwarding only, no shell; even if key is leaked, can only forward to localhost:22
StrictHostKeyChecking=yes Pinned host keys prevent MITM attacks
ExitOnForwardFailure=yes Fails immediately if port binding fails

Impact of jump server compromise

An attacker who gains control of the jump server can reach the work server's SSH port via the tunnel, but still needs valid credentials (private key or password) to log in. SSH connections are end-to-end encrypted; the jump server cannot decrypt or hijack existing sessions.

Primary risks:

  • Brute-force SSH attacks or vulnerability exploitation against the work server
  • Tunnel disruption causing denial of service

Mitigation:

  • Keep the jump server minimal
  • Use strong key-based authentication and disable password login on the work server
  • Use iptables to restrict tunnel user access to tunnel ports only:
sudo iptables -A OUTPUT -p tcp --match multiport --dports 10001:10010 \
    -o lo -m owner ! --uid-owner tunnel -j DROP

Connection Mode Comparison

Mode Security Complexity Description
ProxyJump (recommended) High Low Tunnel ports bound to loopback only; users connect via SSH ProxyJump
Direct connect Lower Medium Requires GatewayPorts=clientspecified; ports exposed to public

ProxyJump mode user SSH config example:

Host my-server
    HostName localhost
    Port 10001
    ProxyJump tunnel@203.0.113.50
    User zhangsan
    ServerAliveInterval 30

For direct-connect mode, set BIND_PREFIX=0.0.0.0: in tunnel.conf and have users set HostName to the jump server address directly.

Server already has a public IP?

If the target server already has a public IP and you just need to bounce through a jump server for IP whitelisting, you don't need a reverse tunnel. Use standard SSH ProxyJump directly:

Host jump-server
    HostName 100.100.100.100
    User <jump account>
    IdentityFile "<path to private key>"

Host work-server
    HostName 200.200.200.200
    User <target account>
    IdentityFile "<path to private key>"
    ProxyJump jump-server

CLI equivalent: ssh -J jump-account@100.100.100.100 target-account@200.200.200.200

Script Source Code

The following scripts are from a real deployment (sanitized). Adapt service names, relay server addresses, etc. to your environment.

setup.sh — Initial setup
#!/usr/bin/env bash
# ============================================================
# Reverse SSH Tunnel — 首次安装
# 需 sudo 执行,仅运行一次
# ============================================================
set -euo pipefail

if [ "$(id -u)" -ne 0 ]; then
    echo "[!] 请用 sudo 执行"
    exit 1
fi

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
AUTOSSH_DIR="/etc/autossh"
SERVICE_SRC="${SCRIPT_DIR}/tunnel@.service"
ENV_SRC="${SCRIPT_DIR}/tunnel.conf"

echo "===== Reverse SSH Tunnel 安装 ====="
echo ""

# ---- 1. 创建专用系统用户 ----
echo "[1/8] 创建 tunnel-runner 系统用户 ..."
if id tunnel-runner &>/dev/null; then
    echo "    ✓ 已存在"
else
    useradd -r -s /usr/sbin/nologin -d "${AUTOSSH_DIR}" tunnel-runner
    echo "    ✓ 已创建"
fi

# ---- 2. 创建目录 ----
echo "[2/8] 创建 ${AUTOSSH_DIR}/ ..."
mkdir -p "${AUTOSSH_DIR}"

# ---- 3. 生成隧道密钥 ----
echo "[3/8] 生成隧道密钥 ..."
if [ -f "${AUTOSSH_DIR}/tunnel_key" ]; then
    echo "    ✓ 隧道密钥已存在,跳过"
else
    ssh-keygen -t ed25519 -f "${AUTOSSH_DIR}/tunnel_key" -N "" -C "tunnel-main"
    echo "    ✓ 已生成"
fi

# ---- 4. 生成管理密钥(用于推送/删除用户公钥到跳板服务器)----
echo "[4/8] 生成管理密钥 ..."
if [ -f "${AUTOSSH_DIR}/management_key" ]; then
    echo "    ✓ 管理密钥已存在,跳过"
else
    ssh-keygen -t ed25519 -f "${AUTOSSH_DIR}/management_key" -N "" -C "tunnel-management"
    echo "    ✓ 已生成"
fi

# ---- 5. 获取跳板服务器主机密钥 ----
echo "[5/8] 获取跳板服务器主机密钥 ..."
source "${ENV_SRC}"
if [ -f "${AUTOSSH_DIR}/known_hosts" ] && grep -q "${RELAY_HOST}" "${AUTOSSH_DIR}/known_hosts"; then
    echo "    ✓ known_hosts 已存在"
else
    ssh-keyscan -p "${RELAY_PORT}" "${RELAY_HOST}" > "${AUTOSSH_DIR}/known_hosts" 2>/dev/null
    LINES=$(wc -l < "${AUTOSSH_DIR}/known_hosts")
    if [ "${LINES}" -eq 0 ]; then
        echo "    ✗ 无法获取主机密钥,检查跳板服务器是否可达"
        exit 1
    fi
    echo "    ✓ 获取 ${LINES} 个密钥"
fi

# ---- 6. 安装配置文件 ----
echo "[6/8] 安装配置文件 ..."
cp "${ENV_SRC}" "${AUTOSSH_DIR}/tunnel.conf"
cp "${SERVICE_SRC}" /etc/systemd/system/tunnel@.service
echo "    ✓ tunnel.conf + tunnel@.service 已安装"

if [ ! -f "${AUTOSSH_DIR}/port-registry.txt" ]; then
    TODAY=$(date '+%Y-%m-%d')
    ADMIN_USER=$(logname 2>/dev/null || echo "admin")
    cat > "${AUTOSSH_DIR}/port-registry.txt" << EOF
# 端口分配表
# 格式:端口  用户名  创建日期  备注
10001   ${ADMIN_USER}   ${TODAY}    管理员
EOF
    echo "    ✓ 端口分配表已创建"
else
    echo "    ✓ 端口分配表已存在"
fi

# ---- 7. 设置权限 ----
echo "[7/8] 设置文件权限 ..."
chown tunnel-runner:tunnel-runner "${AUTOSSH_DIR}/tunnel_key"
chmod 600 "${AUTOSSH_DIR}/tunnel_key"
chown root:root "${AUTOSSH_DIR}/management_key"
chmod 600 "${AUTOSSH_DIR}/management_key"
chown root:root "${AUTOSSH_DIR}/tunnel_key.pub" "${AUTOSSH_DIR}/management_key.pub" \
    "${AUTOSSH_DIR}/known_hosts" "${AUTOSSH_DIR}/tunnel.conf" "${AUTOSSH_DIR}/port-registry.txt"
chmod 644 "${AUTOSSH_DIR}/tunnel_key.pub" "${AUTOSSH_DIR}/management_key.pub" \
    "${AUTOSSH_DIR}/known_hosts" "${AUTOSSH_DIR}/tunnel.conf" "${AUTOSSH_DIR}/port-registry.txt"
echo "    ✓ 权限已设置"

# ---- 8. 停旧进程、启动服务 ----
echo "[8/8] 停旧进程并启动服务 ..."
OLD_PIDS=$(pgrep -f "autossh.*-R" 2>/dev/null || true)
if [ -n "${OLD_PIDS}" ]; then
    echo "    停止旧 autossh 进程: ${OLD_PIDS}"
    kill ${OLD_PIDS} 2>/dev/null || true
    sleep 1
fi

systemctl daemon-reload

FIRST_PORT=$(awk '/^[0-9]/{print $1; exit}' "${AUTOSSH_DIR}/port-registry.txt")
if [ -n "${FIRST_PORT}" ]; then
    systemctl enable "tunnel@${FIRST_PORT}"
    systemctl start "tunnel@${FIRST_PORT}"
    sleep 2
    if systemctl is-active --quiet "tunnel@${FIRST_PORT}"; then
        echo "    ✓ tunnel@${FIRST_PORT} 已启动"
    else
        echo "    ✗ 启动失败(公钥可能尚未添加到跳板服务器)"
        echo "    查看日志: journalctl -u tunnel@${FIRST_PORT} --no-pager -n 20"
    fi
fi

echo ""
echo "===== 安装完成 ====="
echo ""
echo "【下一步】在跳板服务器上执行以下命令:"
echo ""
echo "  ─── 1. 创建 tunnel 用户 ───"
echo "  sudo useradd -m -s /usr/sbin/nologin tunnel"
echo "  sudo mkdir -p /home/tunnel/.ssh"
echo ""
echo "  ─── 2. 写入隧道公钥(用来建隧道的)───"
echo "  echo 'restrict,port-forwarding,permitopen=\"localhost:22\" $(cat ${AUTOSSH_DIR}/tunnel_key.pub)' | \\"
echo "      sudo tee /home/tunnel/.ssh/authorized_keys"
echo ""
echo "  ─── 3. 将管理公钥加入 root 的 authorized_keys ───"
echo "  echo '$(cat ${AUTOSSH_DIR}/management_key.pub)' | \\"
echo "      sudo tee -a /root/.ssh/authorized_keys"
echo ""
echo "  sudo chown -R tunnel:tunnel /home/tunnel/.ssh"
echo "  sudo chmod 700 /home/tunnel/.ssh"
echo "  sudo chmod 600 /home/tunnel/.ssh/authorized_keys"
add-user.sh — Add user
#!/usr/bin/env bash
# ============================================================
# 新增隧道用户 + 推送公钥到跳板服务器
# 用法: sudo ./add-user.sh <用户名> --key-file <公钥文件> | --key <公钥字符串> [--port 端口] [--remark 备注]
# ============================================================
set -euo pipefail

if [ "$(id -u)" -ne 0 ]; then
    echo "[!] 请用 sudo 执行"
    exit 1
fi

# ---- 解析参数 ----
USERNAME=""
PUBKEY_FILE=""
PUBKEY_INLINE=""
PORT=""
REMARK=""

while [ $# -gt 0 ]; do
    case "$1" in
        --key-file)
            PUBKEY_FILE="${2:?--key-file 需要文件路径}"
            shift 2
            ;;
        --key)
            PUBKEY_INLINE="${2:?--key 需要公钥字符串}"
            shift 2
            ;;
        --port)
            PORT="${2:?--port 需要端口号}"
            shift 2
            ;;
        --remark)
            REMARK="${2:?--remark 需要备注文本}"
            shift 2
            ;;
        -*)
            echo "[!] 未知选项: $1"
            exit 1
            ;;
        *)
            if [ -z "${USERNAME}" ]; then
                USERNAME="$1"
            else
                echo "[!] 多余参数: $1"
                exit 1
            fi
            shift
            ;;
    esac
done

if [ -z "${USERNAME}" ]; then
    echo "用法: sudo ./add-user.sh <用户名> --key-file <公钥文件> | --key <公钥字符串> [--port 端口] [--remark 备注]"
    exit 1
fi

if [ -n "${PUBKEY_FILE}" ] && [ -n "${PUBKEY_INLINE}" ]; then
    echo "[!] --key-file 和 --key 不能同时使用"
    exit 1
fi

# ---- 加载配置 ----
source /etc/autossh/tunnel.conf
REGISTRY="/etc/autossh/port-registry.txt"

# ---- 输入校验 ----
if ! [[ "${USERNAME}" =~ ^[a-z_][a-z0-9_-]*$ ]]; then
    echo "[!] 用户名不合法,仅允许小写字母、数字、下划线、连字符"
    exit 1
fi

if grep -qP "^\d+\s+${USERNAME}\b" "${REGISTRY}" 2>/dev/null; then
    echo "[!] 用户 ${USERNAME} 已存在于端口分配表:"
    grep -P "^\d+\s+${USERNAME}\b" "${REGISTRY}"
    exit 1
fi

# 校验公钥
if [ -n "${PUBKEY_INLINE}" ]; then
    PUBKEY_CONTENT=$(echo "${PUBKEY_INLINE}" | grep -v '^#' | grep -v '^$' | head -1)
    if ! echo "${PUBKEY_CONTENT}" | grep -qE '^(ssh-(ed25519|rsa|ecdsa)|ecdsa-sha2)'; then
        echo "[!] 公钥字符串格式不正确"
        exit 1
    fi
elif [ -n "${PUBKEY_FILE}" ]; then
    if [ ! -f "${PUBKEY_FILE}" ]; then
        echo "[!] 公钥文件不存在: ${PUBKEY_FILE}"
        exit 1
    fi
    PUBKEY_CONTENT=$(grep -v '^#' "${PUBKEY_FILE}" | grep -v '^$' | head -1)
    if ! echo "${PUBKEY_CONTENT}" | grep -qE '^(ssh-(ed25519|rsa|ecdsa)|ecdsa-sha2)'; then
        echo "[!] 文件内容不像 SSH 公钥: ${PUBKEY_FILE}"
        exit 1
    fi
else
    PUBKEY_CONTENT=""
    echo "[*] 未提供公钥,将只创建用户和隧道,不推送公钥到跳板服务器"
    echo "    后续可用: sudo ./push-key.sh ${USERNAME} --key-file <公钥文件>"
fi

# 自动分配端口
if [ -z "${PORT}" ]; then
    MAX_PORT=$(awk '/^[0-9]/{print $1}' "${REGISTRY}" | sort -n | tail -1)
    MAX_PORT=${MAX_PORT:-10000}
    PORT=$((MAX_PORT + 1))
    echo "[*] 自动分配端口: ${PORT}"
fi

if ! [[ "${PORT}" =~ ^[0-9]+$ ]] || [ "${PORT}" -lt 1024 ] || [ "${PORT}" -gt 65535 ]; then
    echo "[!] 端口号必须在 1024-65535 之间"
    exit 1
fi

if grep -qP "^${PORT}\s" "${REGISTRY}" 2>/dev/null; then
    echo "[!] 端口 ${PORT} 已被占用:"
    grep -P "^${PORT}\s" "${REGISTRY}"
    exit 1
fi

# ---- 创建系统用户 ----
if ! id "${USERNAME}" &>/dev/null; then
    echo "[*] 创建系统用户 ${USERNAME} ..."
    useradd -m -s /bin/bash "${USERNAME}"
    USER_HOME=$(eval echo "~${USERNAME}")
    mkdir -p "${USER_HOME}/.ssh"
    touch "${USER_HOME}/.ssh/authorized_keys"
    chmod 700 "${USER_HOME}/.ssh"
    chmod 600 "${USER_HOME}/.ssh/authorized_keys"
    chown -R "${USERNAME}:${USERNAME}" "${USER_HOME}/.ssh"
    echo "    ✓ 用户已创建"
else
    echo "[*] 用户 ${USERNAME} 已存在,跳过创建"
    USER_HOME=$(eval echo "~${USERNAME}")
    mkdir -p "${USER_HOME}/.ssh"
    touch "${USER_HOME}/.ssh/authorized_keys"
    chmod 700 "${USER_HOME}/.ssh"
    chmod 600 "${USER_HOME}/.ssh/authorized_keys"
    chown -R "${USERNAME}:${USERNAME}" "${USER_HOME}/.ssh"
fi

# ---- 写入公钥到工作服务器 ----
if [ -n "${PUBKEY_CONTENT}" ]; then
    USER_HOME=$(eval echo "~${USERNAME}")
    if grep -qF "${PUBKEY_CONTENT}" "${USER_HOME}/.ssh/authorized_keys" 2>/dev/null; then
        echo "[*] 公钥已存在于 ${USERNAME} 的 authorized_keys"
    else
        echo "${PUBKEY_CONTENT}" >> "${USER_HOME}/.ssh/authorized_keys"
        chown "${USERNAME}:${USERNAME}" "${USER_HOME}/.ssh/authorized_keys"
        echo "[✓] 公钥已写入 ${USER_HOME}/.ssh/authorized_keys"
    fi
fi

# ---- 推送公钥到跳板服务器 ----
if [ -n "${PUBKEY_CONTENT}" ]; then
    echo "[*] 推送公钥到跳板服务器 ..."
    RELAY_AK_ENTRY="restrict,port-forwarding,permitopen=\"localhost:${PORT}\" ${PUBKEY_CONTENT}"

    SSH_CMD="ssh -i ${MGMT_KEY} \
        -o Port=${RELAY_PORT} \
        -o StrictHostKeyChecking=yes \
        -o UserKnownHostsFile=/etc/autossh/known_hosts \
        -o IdentitiesOnly=yes \
        -o ConnectTimeout=15 \
        ${RELAY_MGMT_USER}@${RELAY_HOST}"

    if ${SSH_CMD} "grep -qF '${PUBKEY_CONTENT}' /home/tunnel/.ssh/authorized_keys 2>/dev/null"; then
        echo "    ✓ 公钥已存在于跳板服务器"
    else
        ${SSH_CMD} "echo '${RELAY_AK_ENTRY}' >> /home/tunnel/.ssh/authorized_keys" && \
            echo "    ✓ 公钥已推送到跳板服务器 tunnel authorized_keys" || \
            echo "    [!] 推送失败 — 管理密钥可能未添加到跳板服务器 root 的 authorized_keys"
    fi
fi

# ---- 更新端口分配表 ----
TODAY=$(date '+%Y-%m-%d')
printf "%s\t%s\t%s\t%s\n" "${PORT}" "${USERNAME}" "${TODAY}" "${REMARK}" >> "${REGISTRY}"
echo "[✓] 端口分配表已更新"

# ---- 启动隧道实例 ----
echo "[*] 启动 tunnel@${PORT} ..."
systemctl enable "tunnel@${PORT}" 2>/dev/null
systemctl start "tunnel@${PORT}"
sleep 2

if systemctl is-active --quiet "tunnel@${PORT}"; then
    echo "[✓] 隧道已启动"
else
    echo "[✗] 隧道启动失败:"
    journalctl -u "tunnel@${PORT}" --no-pager -n 10
    exit 1
fi

# ---- 输出客户端配置 ----
echo ""
echo "===== 用户 ${USERNAME} 已添加 (端口 ${PORT}) ====="
echo ""
echo "将以下配置发给 ${USERNAME},添加到 ~/.ssh/config:"
echo ""
echo "  Host my-server"
echo "      HostName localhost"
echo "      Port ${PORT}"
echo "      ProxyJump tunnel@${RELAY_HOST}"
echo "      User ${USERNAME}"
echo "      ServerAliveInterval 30"
remove-user.sh — Remove user
#!/usr/bin/env bash
# ============================================================
# 删除隧道用户 + 从跳板服务器清除公钥
# 用法: sudo ./remove-user.sh <用户名> [--delete-home]
# ============================================================
set -euo pipefail

if [ "$(id -u)" -ne 0 ]; then
    echo "[!] 请用 sudo 执行"
    exit 1
fi

USERNAME="${1:?用法: sudo ./remove-user.sh <用户名> [--delete-home]}"
DELETE_HOME="${2:-}"

source /etc/autossh/tunnel.conf
REGISTRY="/etc/autossh/port-registry.txt"

# 查找端口
PORT=$(awk -v u="${USERNAME}" '$2 == u {print $1}' "${REGISTRY}")
if [ -z "${PORT}" ]; then
    echo "[!] 用户 ${USERNAME} 不在端口分配表中"
    exit 1
fi

echo "[*] 用户 ${USERNAME} — 端口 ${PORT}"
echo "[!] 确认移除?(y/N)"
read -r CONFIRM
if [ "${CONFIRM}" != "y" ] && [ "${CONFIRM}" != "Y" ]; then
    echo "已取消"
    exit 0
fi

# ---- 停止并禁用隧道实例 ----
echo "[*] 停止 tunnel@${PORT} ..."
systemctl disable --now "tunnel@${PORT}" 2>/dev/null || true
echo "    ✓ 已停止"

# ---- 从跳板服务器删除公钥 ----
echo "[*] 从跳板服务器删除公钥 (匹配 permitopen 端口 ${PORT}) ..."
SSH_CMD="ssh -i ${MGMT_KEY} \
    -o Port=${RELAY_PORT} \
    -o StrictHostKeyChecking=yes \
    -o UserKnownHostsFile=/etc/autossh/known_hosts \
    -o IdentitiesOnly=yes \
    -o ConnectTimeout=15 \
    ${RELAY_MGMT_USER}@${RELAY_HOST}"

# 匹配包含 permitopen="localhost:PORT" 的行并删除
if ${SSH_CMD} "sed -i '/permitopen=\"localhost:${PORT}\"/d' /home/tunnel/.ssh/authorized_keys" 2>/dev/null; then
    echo "    ✓ 已从跳板服务器删除"
else
    echo "    [!] 删除失败或无匹配条目 — 管理密钥可能未配置"
fi

# ---- 从端口分配表移除 ----
sed -i "/^\s*${PORT}\s\+/d" "${REGISTRY}"
echo "    ✓ 端口分配表已更新"

# ---- 清理用户 ----
if [ "${DELETE_HOME}" = "--delete-home" ]; then
    echo "[*] 删除用户 ${USERNAME} 及主目录 ..."
    userdel -r "${USERNAME}" 2>/dev/null && echo "    ✓ 已删除" || echo "    ! 用户可能不存在"
else
    echo "[*] 保留 ${USERNAME} 的系统账户和数据"
    echo "    如需删除: sudo userdel -r ${USERNAME}"
fi

echo ""
echo "===== ${USERNAME} 已从隧道移除 (端口 ${PORT}) ====="
push-key.sh — Push/update public key
#!/usr/bin/env bash
# ============================================================
# 单独推送/更新用户公钥(用于 add-user 时未提供密钥的场景)
# 用法: sudo ./push-key.sh <用户名> --key-file <公钥文件>
#       sudo ./push-key.sh <用户名> --key <公钥字符串>
# ============================================================
set -euo pipefail

if [ "$(id -u)" -ne 0 ]; then
    echo "[!] 请用 sudo 执行"
    exit 1
fi

USERNAME=""
PUBKEY_FILE=""
PUBKEY_INLINE=""

# 第一个非选项参数是用户名
while [ $# -gt 0 ]; do
    case "$1" in
        --key-file)
            PUBKEY_FILE="${2:?--key-file 需要文件路径}"
            shift 2
            ;;
        --key)
            PUBKEY_INLINE="${2:?--key 需要公钥字符串}"
            shift 2
            ;;
        -*)
            echo "[!] 未知选项: $1"
            exit 1
            ;;
        *)
            if [ -z "${USERNAME}" ]; then
                USERNAME="$1"
            else
                echo "[!] 多余参数: $1"
                exit 1
            fi
            shift
            ;;
    esac
done

if [ -z "${USERNAME}" ]; then
    echo "用法: sudo ./push-key.sh <用户名> --key-file <公钥文件>"
    echo "      sudo ./push-key.sh <用户名> --key <公钥字符串>"
    exit 1
fi

if [ -z "${PUBKEY_FILE}" ] && [ -z "${PUBKEY_INLINE}" ]; then
    echo "[!] 请提供 --key-file 或 --key"
    exit 1
fi

if [ -n "${PUBKEY_FILE}" ] && [ -n "${PUBKEY_INLINE}" ]; then
    echo "[!] --key-file 和 --key 不能同时使用"
    exit 1
fi

source /etc/autossh/tunnel.conf
REGISTRY="/etc/autossh/port-registry.txt"

# 查找端口
PORT=$(awk -v u="${USERNAME}" '$2 == u {print $1}' "${REGISTRY}")
if [ -z "${PORT}" ]; then
    echo "[!] 用户 ${USERNAME} 不在端口分配表中"
    exit 1
fi

# 获取公钥内容
if [ -n "${PUBKEY_INLINE}" ]; then
    PUBKEY_CONTENT=$(echo "${PUBKEY_INLINE}" | grep -v '^#' | grep -v '^$' | head -1)
    if ! echo "${PUBKEY_CONTENT}" | grep -qE '^(ssh-(ed25519|rsa|ecdsa)|ecdsa-sha2)'; then
        echo "[!] 公钥字符串格式不正确"
        exit 1
    fi
else
    if [ ! -f "${PUBKEY_FILE}" ]; then
        echo "[!] 公钥文件不存在: ${PUBKEY_FILE}"
        exit 1
    fi
    PUBKEY_CONTENT=$(grep -v '^#' "${PUBKEY_FILE}" | grep -v '^$' | head -1)
    if ! echo "${PUBKEY_CONTENT}" | grep -qE '^(ssh-(ed25519|rsa|ecdsa)|ecdsa-sha2)'; then
        echo "[!] 文件内容不像 SSH 公钥"
        exit 1
    fi
fi

# ---- 写入工作服务器 authorized_keys ----
USER_HOME=$(eval echo "~${USERNAME}")
if [ -d "${USER_HOME}/.ssh" ]; then
    if grep -qF "${PUBKEY_CONTENT}" "${USER_HOME}/.ssh/authorized_keys" 2>/dev/null; then
        echo "[*] 公钥已存在于工作服务器 authorized_keys"
    else
        echo "${PUBKEY_CONTENT}" >> "${USER_HOME}/.ssh/authorized_keys"
        chown "${USERNAME}:${USERNAME}" "${USER_HOME}/.ssh/authorized_keys"
        echo "[✓] 公钥已写入工作服务器 ${USERNAME} 的 authorized_keys"
    fi
fi

# ---- 推送到跳板服务器 ----
echo "[*] 推送公钥到跳板服务器 (端口 ${PORT}) ..."
RELAY_AK_ENTRY="restrict,port-forwarding,permitopen=\"localhost:${PORT}\" ${PUBKEY_CONTENT}"

SSH_CMD="ssh -i ${MGMT_KEY} \
    -o Port=${RELAY_PORT} \
    -o StrictHostKeyChecking=yes \
    -o UserKnownHostsFile=/etc/autossh/known_hosts \
    -o IdentitiesOnly=yes \
    -o ConnectTimeout=15 \
    ${RELAY_MGMT_USER}@${RELAY_HOST}"

if ${SSH_CMD} "grep -qF '${PUBKEY_CONTENT}' /home/tunnel/.ssh/authorized_keys 2>/dev/null"; then
    echo "    ✓ 公钥已存在于跳板服务器"
else
    ${SSH_CMD} "echo '${RELAY_AK_ENTRY}' >> /home/tunnel/.ssh/authorized_keys" && \
        echo "[✓] 公钥已推送到跳板服务器" || \
        echo "[!] 推送失败 — 检查管理密钥是否已添加到跳板服务器"
fi

echo ""
echo "===== 完成 ====="
echo "用户 ${USERNAME} (端口 ${PORT}) 的 SSH config:"
echo ""
echo "  Host my-server"
echo "      HostName localhost"
echo "      Port ${PORT}"
echo "      ProxyJump tunnel@${RELAY_HOST}"
echo "      User ${USERNAME}"
echo "      ServerAliveInterval 30"