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:
- SSH client authenticates to jump server's
tunneluser using the user's private key (port-forwarding only) - Through the tunnel, reaches work server's port 22
- 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:
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¶
-
Copy the script directory (containing
setup.sh,tunnel.conf,tunnel@.service, etc.) to the work server -
Edit
tunnel.confwith 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 -
Run the setup script:
The script automatically:
- Creates the
tunnel-runnersystem 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
- Creates the
-
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¶
The script automatically:
- Creates a system user on the work server (if it doesn't exist)
- Writes the user's public key to
~/.ssh/authorized_keys - Pushes the key to the jump server (with
restrict,port-forwarding,permitopenrestrictions) - Updates the port registry
/etc/autossh/port-registry.txt - 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:
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:
- Stop and disable the tunnel instance for that port
- Remove the corresponding authorized_keys entry from the jump server (matches
permitopenport) - 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:
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:
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:
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"