SSH & autossh — Port forwarding (Local, Remote, Dynamic) và các tham số quan trọng
Ý kiến
0
Chưa có ý kiến nào. Hãy là người đầu tiên chia sẻ!
Chưa có ý kiến nào. Hãy là người đầu tiên chia sẻ!
SSH tunnel là con dao nhỏ gọn nhất trong toolbox của DevOps: truy cập database nội bộ qua bastion, expose dịch vụ local ra Internet, đi vòng qua firewall hạn chế, mở SOCKS proxy để duyệt qua đầu kia. Nhưng SSH tunnel chỉ tốt khi nó không tự rớt — và đây là chỗ autossh bù vào: giám sát ssh, tự restart khi kết nối chết.
Bài này tổng hợp 3 mode forwarding (-L, -R, -D), các tham số đi kèm bắt buộc phải nhớ (-f -N -T, ServerAliveInterval, ExitOnForwardFailure, GatewayPorts), và autossh với -M 0 + biến môi trường. Mọi lệnh trong bài đã được test trực tiếp bằng Docker container linuxserver/openssh-server + Python http.server hai phía host/container.
Trước khi đi vào lệnh, phải hiểu hướng đi của data. Gọi A là máy bạn gõ ssh, B là máy bạn ssh vào.
| Mode | Cờ | Mở port ở | Đích đến (forward đến) | Use case điển hình |
|---|---|---|---|---|
| Local | -L | Máy A (local) | Một địa chỉ nhìn từ B | Truy cập DB nội bộ qua bastion |
| Remote | -R | Máy B (remote) | Một địa chỉ nhìn từ A | Expose dev server local ra VPS public |
| Dynamic | -D | Máy A (local) | Bất kỳ đâu — SOCKS proxy | Duyệt web/đi proxy qua B |
Cú pháp chung:
ssh -L [bind_addr:]LOCAL_PORT:DEST_HOST:DEST_PORT user@B
ssh -R [bind_addr:]REMOTE_PORT:DEST_HOST:DEST_PORT user@B
ssh -D [bind_addr:]LOCAL_PORT user@B
Quan trọng: DEST_HOST trong -L được resolve từ phía B (sau khi data đi qua tunnel). Ngược lại, DEST_HOST trong -R resolve từ phía A. Nhầm 2 cái này là lỗi #1 khi mới dùng tunnel.
-L) — truy cập dịch vụ phía sau bastionTình huống kinh điển: PostgreSQL chạy trong VPC private, chỉ bastion (B) ssh vào được. Bạn muốn dùng DBeaver/psql từ máy laptop (A) connect vào nó.
ssh -fN -L 15432:db.internal:5432 [email protected]
psql -h 127.0.0.1 -p 15432 -U app appdb
Khi DBeaver/psql kết nối tới 127.0.0.1:15432 trên A, SSH bốc gói tin, chui qua tunnel, tới B, B mở socket tới db.internal:5432 — đúng như B đang nói chuyện với DB.
Mặc định -L bind vào localhost (an toàn). Nếu bạn muốn share tunnel cho máy khác trong LAN, prefix bind address:
# Chỉ máy A dùng được (mặc định, an toàn)
ssh -fN -L 15432:db.internal:5432 user@bastion
# Cho mọi interface — đồng nghiệp trên LAN cũng connect được
ssh -fN -L 0.0.0.0:15432:db.internal:5432 user@bastion
Mở 0.0.0.0 nghĩa là ai đó trong cùng LAN cũng có thể truy cập DB qua máy bạn — chỉ làm khi cần thiết và bạn hiểu rủi ro.
Lặp -L bao nhiêu lần tùy thích:
ssh -fN \
-L 15432:db.internal:5432 \
-L 16379:redis.internal:6379 \
-L 19200:es.internal:9200 \
user@bastion
-R) — expose máy local ra ngoàiBạn đang dev Next.js trên laptop ở localhost:3000, muốn cho đồng nghiệp test mà không deploy. Bạn có một VPS công khai vps.example.com:
ssh -fN -R 8080:127.0.0.1:3000 [email protected]
Mọi request tới vps.example.com:8080 đi qua tunnel về laptop, vào localhost:3000. Đây chính là cách ngrok/cloudflared tunnel hoạt động bên dưới — chỉ khác là chúng tự lo TLS + subdomain.
-R bind loopback ở BĐây là chỗ rất nhiều người vấp. Lệnh trên về mặc định chỉ làm B mở port trên loopback (127.0.0.1:8080) — đồng nghiệp ngoài Internet vẫn không vào được. Test trực tiếp trong môi trường Docker:
# -R 19093 mặc định -> server netstat:
tcp 0 0 127.0.0.1:19093 0.0.0.0:* LISTEN
tcp 0 0 ::1:19093 :::* LISTEN
Để bind ra mọi interface, cần cả hai điều kiện:
Server sshd_config có GatewayPorts clientspecified (hoặc yes).
Client truyền -R 0.0.0.0:8080:127.0.0.1:3000.
# Trên VPS, sửa /etc/ssh/sshd_config:
GatewayPorts clientspecified
# Reload sshd: systemctl reload ssh
# Trên laptop:
ssh -fN -R 0.0.0.0:8080:127.0.0.1:3000 [email protected]
# Server netstat sau khi sửa:
# tcp 0 0 0.0.0.0:19094 0.0.0.0:* LISTEN ✓
Giá trị GatewayPorts | Hành vi |
|---|---|
no (mặc định) | Mọi -R đều bind 127.0.0.1 — bind 0.0.0.0 bị từ chối |
clientspecified | Client quyết định — không có bind addr thì 127.0.0.1, có thì theo client |
yes | Mọi -R đều bind 0.0.0.0 (ép buộc) |
-D) — SOCKS5 proxyKhi bạn muốn duyệt nhiều đích khác nhau qua B (vd: truy cập toàn bộ mạng nội bộ company qua bastion), không thực tế để mở -L cho từng host. -D mở một SOCKS5 proxy local — mọi app hỗ trợ SOCKS đều dùng được:
ssh -fN -D 1080 user@bastion
Cấu hình app (Firefox: Settings → Network Settings → Manual SOCKS5 proxy 127.0.0.1:1080), hoặc dùng curl trực tiếp:
curl --socks5-hostname 127.0.0.1:1080 http://intranet.internal/
Chú ý dùng --socks5-hostname chứ không phải --socks5 — flag -hostname bảo curl gửi cả hostname qua proxy để DNS resolve từ phía bastion, đảm bảo tên nội bộ kiểu db.internal giải được.
Riêng -L/-R/-D chưa đủ. Trong thực tế bạn luôn dùng kèm:
| Cờ | Ý nghĩa | Khi nào dùng |
|---|---|---|
-f | Đẩy ssh xuống background sau khi auth xong | Tunnel chạy nền — không muốn giữ terminal |
-N | Không chạy lệnh remote nào (chỉ forward) | Luôn dùng với tunnel — đỡ tốn shell remote |
-T | Không cấp pseudo-TTY | Đi với -f -N — gọn hơn vài byte/giây |
-i KEY | Chỉ định private key | Khi key không phải ~/.ssh/id_* mặc định |
-p PORT | SSH port của B (mặc định 22) | Server đổi port |
Combo "tunnel chạy nền chuẩn":
ssh -fNT -i ~/.ssh/key -p 22 -L 15432:db.internal:5432 user@bastion
ServerAliveInterval / ServerAliveCountMax — phát hiện kết nối chếtSSH client không tự biết server biến mất (firewall NAT bỏ entry, máy server mất điện). Nó cứ tưởng kết nối vẫn còn — tunnel im lặng treo. Hai option này bắt client gửi keepalive định kỳ và đóng kết nối nếu không thấy phản hồi:
ssh -fNT \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-L 15432:db.internal:5432 user@bastion
ServerAliveInterval=30: 30s không có traffic thì gửi keepalive.
ServerAliveCountMax=3: gửi 3 keepalive liên tiếp không phản hồi (=90s) thì ssh tự exit với status 255.
Đây chính là tín hiệu mà autossh bắt để biết "đến lúc restart" — bắt buộc phải có khi dùng với autossh.
ExitOnForwardFailure=yes — đừng chạy nếu forward failMặc định, nếu ssh không bind được local port (port đã có người dùng), nó vẫn chạy tiếp — bạn nghĩ tunnel đang lên nhưng thực tế chẳng có gì. Option này ép ssh exit ngay khi setup forward thất bại:
ssh -fNT -o ExitOnForwardFailure=yes -L 15432:db.internal:5432 user@bastion
# stderr nếu port bị chiếm:
# bind [127.0.0.1]:15432: Address already in use
# channel_setup_fwd_listener_tcpip: cannot listen to port: 15432
# Could not request local forwarding.
Không có option này, ssh sẽ in cảnh báo rồi vẫn chạy nền — autossh không thấy lỗi, không restart, mà tunnel thì chết.
-o StrictHostKeyChecking=accept-new — tự thêm host key mới vào known_hosts (không hỏi tay), nhưng vẫn block nếu key đã biết bị đổi. An toàn hơn =no.
-o ConnectTimeout=10 — bỏ cuộc nếu TCP handshake quá 10s.
-o TCPKeepAlive=yes — bật TCP-level keepalive (khác với ServerAlive nằm trên SSH layer).
-C — bật nén; chỉ có lợi khi qua link chậm + dữ liệu nén tốt (HTML, JSON). Với binary/video, tắt đi.
SSH client tự nó không reconnect. Khi mạng chập chờn, ssh chết là tunnel chết. autossh là wrapper bao quanh ssh: nó spawn ssh, giám sát, và respawn khi ssh thoát với lỗi.
brew install autossh # macOS
apt install autossh # Debian/Ubuntu
dnf install autossh # Fedora/RHEL
-M 0 — pattern khuyên dùng hiện nayTài liệu cũ thường viết -M 20000 — autossh mở một port giám sát riêng và echo data qua nó. Cách này có vấn đề: cần thêm 2 port forward, và đôi khi false positive. Pattern hiện đại là -M 0 — tắt monitor port, để ServerAliveInterval đảm nhiệm việc phát hiện chết:
autossh -M 0 -f -N \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
-L 15432:db.internal:5432 user@bastion
Cờ -f ở đây thuộc autossh, không phải ssh — autossh tự fork. Khi đó AUTOSSH_GATETIME được set về 0 tự động (xem mục 6.2).
| Biến | Mặc định | Ý nghĩa |
|---|---|---|
AUTOSSH_GATETIME | 30 | ssh phải sống tối thiểu N giây thì autossh mới coi là "khởi động thành công". Quá ngắn = autossh sẽ bỏ cuộc nếu vài lần đầu fail nhanh. -f tự set về 0. |
AUTOSSH_POLL | 600 | Khoảng cách giữa các lần kiểm tra (giây). Với -M 0 ít quan trọng — ServerAlive đã làm việc đó. |
AUTOSSH_FIRST_POLL | = POLL | Lần kiểm tra đầu tiên — set ngắn để phát hiện lỗi sớm. |
AUTOSSH_MAXSTART | -1 (vô hạn) | Số lần restart tối đa. Đặt số để autossh chịu "thua" sau N lần — tránh chạy mãi khi server biến mất luôn. |
AUTOSSH_LOGFILE | (syslog) | Path file log. Cực hữu ích để debug — không có thì log đi vào syslog. |
AUTOSSH_PIDFILE | — | Ghi pid ra file — dùng để kill sau này. |
AUTOSSH_DEBUG | — | Set để log verbose ra stderr (chỉ debug). |
Ví dụ production-grade:
AUTOSSH_GATETIME=0 \
AUTOSSH_LOGFILE=/var/log/autossh-db.log \
AUTOSSH_PIDFILE=/run/autossh-db.pid \
autossh -M 0 -f -N \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
-o StrictHostKeyChecking=accept-new \
-i /etc/ssh/autossh_key \
-L 15432:db.internal:5432 deploy@bastion
Tunnel đang sống, server bị tắt → autossh thấy ssh exit 255 → restart liên tục → server bật lại → tunnel hồi phục. Log ghi rõ:
autossh: port set to 0, monitoring disabled
autossh: starting ssh (count 1)
autossh: ssh child pid is 27247
autossh: ssh exited with error status 255; restarting ssh
autossh: starting ssh (count 2)
...
autossh: starting ssh (count 31)
# (server đã sống lại, ssh kết nối thành công, tunnel hồi phục)
Với AUTOSSH_MAXSTART=3:
autossh: starting ssh (count 1 of 3)
autossh: ssh exited with error status 255; restarting ssh
autossh: starting ssh (count 2 of 3)
autossh: ssh exited with error status 255; restarting ssh
autossh: starting ssh (count 3 of 3)
autossh: ssh exited with error status 255; restarting ssh
autossh: max start count reached; exiting
Trên server production, không gõ tay rồi để treo — package nó vào systemd:
# /etc/systemd/system/tunnel-db.service
[Unit]
Description=Persistent SSH tunnel to internal DB
After=network-online.target
Wants=network-online.target
[Service]
User=tunnel
Environment=AUTOSSH_GATETIME=0
Environment=AUTOSSH_LOGFILE=/var/log/autossh-db.log
ExecStart=/usr/bin/autossh -M 0 -N \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
-o StrictHostKeyChecking=accept-new \
-i /home/tunnel/.ssh/id_ed25519 \
-L 0.0.0.0:15432:db.internal:5432 \
[email protected]
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
Lưu ý: không dùng -f trong systemd — systemd quản fork-mode sẵn (Type=simple mặc định cần process chạy foreground). -f + systemd = service báo "failed start" vì process gốc thoát ngay.
systemctl daemon-reload
systemctl enable --now tunnel-db.service
systemctl status tunnel-db
channel X: open failed: administratively prohibitedServer đã chặn TCP forwarding. Sửa /etc/ssh/sshd_config:
AllowTcpForwarding yes
# hoặc giới hạn:
AllowTcpForwarding local # chỉ -L
AllowTcpForwarding remote # chỉ -R
Reload sshd sau khi sửa: systemctl reload ssh.
bind: Address already in usePort local đã có người chiếm. Tìm và kill:
lsof -iTCP:15432 -sTCP:LISTEN
# hoặc:
ss -tlnp | grep 15432
Triệu chứng: curl 127.0.0.1:15432 stall mãi. Nguyên nhân thường gặp: DEST_HOST không resolve được từ phía server B. Thử ssh vào B rồi nc -vz db.internal 5432 — nếu fail thì vấn đề ở DNS/network của B, không phải tunnel.
-R bind không ra ngoàiĐã giải thích ở mục 3.1 — quên GatewayPorts clientspecified phía server, hoặc quên 0.0.0.0: phía client.
Thường do StrictHostKeyChecking hỏi và ssh fail ngay (vì autossh chạy nền, không có TTY trả lời prompt). Thêm -o StrictHostKeyChecking=accept-new hoặc đảm bảo host key đã có trong known_hosts trước khi enable service.
3 mode forwarding: -L mở port ở local đi tới đích nhìn từ remote; -R mở port ở remote đi tới đích nhìn từ local; -D mở SOCKS5 proxy ở local.
Combo chuẩn cho tunnel nền: -fNT + ServerAliveInterval=30 + ServerAliveCountMax=3 + ExitOnForwardFailure=yes.
GatewayPorts clientspecified phía server + -R 0.0.0.0:port:... phía client để expose remote forward ra ngoài.
autossh -M 0 là pattern hiện đại — tắt monitor port, dùng ServerAliveInterval để phát hiện kết nối chết.
Biến môi trường autossh quan trọng: AUTOSSH_GATETIME=0 (luôn với -f), AUTOSSH_LOGFILE (debug), AUTOSSH_MAXSTART (đặt giới hạn để không chạy mãi).
Production deploy: gói vào systemd service với Restart=always, bỏ -f để systemd quản fork.
Mọi lệnh trong bài đã được verify bằng container Docker linuxserver/openssh-server + Python http.server — không có lệnh nào "copy từ Google rồi đoán".
Pipeline production Next.js → Docker → K8s với cache image, manual approval. Tổng kết best practices và kỹ thuật debug (CI Lint, visualizer, CI_DEBUG_TRACE).
Khai báo environment để GitLab theo dõi deployments, có lịch sử và nút rollback. Review Apps — tính năng signature: mỗi MR tự deploy lên môi trường tạm.
Tái sử dụng pipeline giữa nhiều dự án với include (local, project, template, remote), kế thừa job với extends (hidden job), và YAML anchors.