SSH
Xem thửTình huống: cùng một dòng Permission denied, năm nguyên nhân khác nhau
Pipeline gdev-ci cần SSH vào VM staging bằng user deploy để chạy script deploy. Sáng nay bước này đỏ với đúng một dòng:
deploy@gdev-vm: Permission denied (publickey).
Bạn đã kiểm tra khóa, vẫn đúng. Một đồng đội hôm trước làm điều ngược lại: tắt đăng nhập bằng mật khẩu trên server, rồi phát hiện khóa của mình chưa được cài, và phải vào console của nhà cung cấp để tự cứu. Cả hai đều rơi vào cùng một cái bẫy: phía client chỉ nhìn thấy một lý do chung chung, còn lý do thật nằm ở log của server.
Bài này dạy cách SSH tin tưởng nhau, cách thiết lập và cách chẩn đoán khi nó từ chối. Sau bài này bạn làm được các việc sau:
- Giải thích vì sao private key không bao giờ rời máy bạn mà vẫn chứng minh được bạn là ai.
- Tạo khóa ed25519, cài vào
authorized_keys, viết~/.ssh/configvớiProxyJump. - Đọc log
sshdbằngjournalctlđể tìm lý do thật củaPermission denied. - Siết cấu hình
sshdmà không tự khóa mình ngoài máy chủ.
Bạn cần có: VM Ubuntu 24.04 từ bài Giới thiệu Linux (cần systemd cho systemctl và journalctl) và bài Lệnh Linux cho DevOps.
Ba mô hình cần nhớ
- Tin tưởng hai chiều: client phải tin server là máy thật (host key, lưu trong
~/.ssh/known_hosts), và server phải tin client là người được phép (public key, lưu trong~/.ssh/authorized_keyscủa tài khoản đích). - Private key chứng minh bằng chữ ký, không bằng cách gửi đi: client ký dữ liệu của phiên bằng private key, server kiểm tra chữ ký bằng public key. Kẻ nghe lén không có gì để dùng lại.
- Client chỉ thấy kết quả, server giữ lý do: sshd cố ý không nói rõ tại sao từ chối cho người ngoài. Muốn biết lý do, đọc log của sshd và nhớ rằng quyền của các file và thư mục là một điều kiện.
Chuẩn bị môi trường lab
Cần một máy Ubuntu 24.04 có systemd. Để một VM đủ cho cả hai vai, ta tạo tài khoản deploy trên chính VM rồi SSH từ user của bạn tới deploy@localhost. Với server thật bạn chỉ thay localhost bằng địa chỉ của nó.
Cài và kiểm tra server SSH. Trên Ubuntu, dịch vụ tên là ssh, không phải sshd:
sudo apt install -y openssh-server
systemctl is-active ssh
sudo adduser --disabled-password --gecos "" deploy
Kết quả: active, và một tài khoản deploy không có mật khẩu (--disabled-password nghĩa là chỉ vào được bằng khóa). Ubuntu 24.04 cho SSH chạy theo kiểu socket activation: đơn vị ssh.socket nghe cổng 22 và gọi ssh.service khi có kết nối đầu tiên, nên nếu is-active báo inactive ngay sau khi cài, thử kết nối một lần rồi kiểm tra lại.
Tạo cặp khóa trên phía client (user của bạn):
ssh-keygen -t ed25519 -a 100 -C "you@gdev-laptop"
ls -l ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pub
ed25519 là thuật toán chữ ký nhỏ, nhanh và an toàn. -a 100 làm khóa private được mã hóa bằng passphrase khó dò hơn. Hãy đặt passphrase cho khóa cá nhân; chỉ khóa chạy trong pipeline mới để trống, và khi đó phải giới hạn quyền như ở phần cuối bài. Kết quả minh họa:
-rw------- 1 you you 399 Oct 23 19:00 /home/you/.ssh/id_ed25519
-rw-r--r-- 1 you you 93 Oct 23 19:00 /home/you/.ssh/id_ed25519.pub
Khóa private có quyền 600 (chỉ chủ đọc ghi), khóa public được phép công khai. Cài public key cho tài khoản deploy. ssh-copy-id cần mật khẩu nên không dùng được cho tài khoản không mật khẩu, ta cài bằng install để đặt luôn chủ sở hữu và quyền:
sudo install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
sudo install -m 600 -o deploy -g deploy ~/.ssh/id_ed25519.pub /home/deploy/.ssh/authorized_keys
ssh deploy@localhost 'whoami; hostname'
Lần đầu, SSH hỏi về host key của server. Kết quả minh họa (fingerprint của bạn sẽ khác):
The authenticity of host 'localhost (127.0.0.1)' can't be established.
ED25519 key fingerprint is SHA256:+5JgUEvOCbzK8JvGG7kD0Dl/zNCBeKtdNvDHJJ1IZAY.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
Đây là chiều tin tưởng thứ nhất. Đừng gõ yes theo phản xạ: so fingerprint này với fingerprint thật của server bằng ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub chạy trên server (qua một kênh khác như console). Sau khi yes, host key được lưu vào ~/.ssh/known_hosts và lần sau không hỏi nữa. Kết quả: in ra deploy và tên máy.
Điều gì xảy ra trong một lần kết nối
Trước khi xem sơ đồ, hãy dự đoán: trong lúc đăng nhập bằng khóa, private key có được gửi lên server không? Nếu không, server làm sao biết bạn là chủ của khóa?
Đang tải sơ đồ…
Cách đọc sơ đồ:
- Hai thông điệp đầu là chiều tin tưởng thứ nhất: server trình host key kèm chữ ký, client đối chiếu với
known_hosts. Nếu khóa khác với lần trước, SSH dừng lại và cảnh báo bạn thay vì tiếp tục. - Từ "offer public key" trở đi là chiều thứ hai. Client chỉ đề nghị một public key. Server tra trong
authorized_keyscủa tài khoảndeploy; nếu có, nó nói "key acceptable" rồi mới yêu cầu chứng minh. - Chứng minh là một chữ ký tạo bằng private key trên dữ liệu của phiên này. Server kiểm tra chữ ký bằng public key đã tra. Private key không đi qua dây, và chữ ký chỉ đúng cho phiên đó nên không dùng lại được.
- Hai mũi tên tới
authorized_keyslà chỗ mà mọi lỗi cấu hình "chỉ server mới biết" xảy ra: file không có key, file không đọc được, hoặc sshd không tin quyền của file.
Một hệ quả thực dụng: mọi thông điệp sau khi trao đổi khóa đều nằm trong kênh mã hóa, và ssh -v in ra từng bước để bạn biết client dừng ở đâu.
Cấu hình client: một chỗ để đặt tên, khóa và đường đi
Gõ đầy đủ địa chỉ, user, khóa mỗi lần vừa dài vừa dễ sai. File ~/.ssh/config gom chúng lại. Mô hình GDev có một bastion (máy trung gian duy nhất lộ ra Internet) và các máy staging nằm trong mạng nội bộ:
Host gdev-bastion
HostName 203.0.113.10
User deploy
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
Host gdev-staging
HostName 10.0.2.15
User deploy
ProxyJump gdev-bastion
Địa chỉ 203.0.113.10 và 10.0.2.15 là địa chỉ minh họa, bạn thay bằng của mình. Kiểm tra cách SSH hiểu cấu hình, không cần kết nối:
ssh -G gdev-staging | grep -E '^(hostname|user|proxyjump) '
Kết quả minh họa:
user deploy
hostname 10.0.2.15
proxyjump gdev-bastion
Giải thích: IdentitiesOnly yes bắt SSH chỉ thử đúng khóa ghi trong IdentityFile, thay vì thử mọi khóa trong agent (thử nhiều khóa có thể chạm giới hạn số lần thử của server và bị ngắt). ProxyJump gdev-bastion mở kết nối SSH tới bastion rồi tạo đường hầm TCP tới máy staging; chạy ssh gdev-staging là đủ.
So với ForwardAgent yes: ProxyJump chỉ chuyển tiếp dữ liệu, chữ ký vẫn được tạo trên laptop và bastion không chạm được vào khóa của bạn. ForwardAgent để bastion dùng agent của bạn trong lúc phiên đang mở, nên ai có quyền root trên bastion có thể mượn nó. Vì vậy mặc định hãy dùng ProxyJump.
Giờ đến phần khó của bài. Dự đoán trước khi bấm: hai nguyên nhân khác nhau (quyền thư mục sai, và user không có trong danh sách cho phép) có để lại cùng một dòng trên client không? Còn trên server?
Đang tải Animation…
Failure lab: tái hiện từng nguyên nhân và đọc log
Mở hai cửa sổ terminal. Cửa sổ một đọc log của server, cửa sổ hai đóng vai client:
sudo journalctl -u ssh -f
Lỗi 1, thư mục home của deploy cho người khác ghi. Ở cửa sổ hai:
sudo chmod 777 /home/deploy
ssh -o BatchMode=yes deploy@localhost true
Client thấy:
deploy@localhost: Permission denied (publickey).
Log của server ghi (phần tiền tố thời gian và PID sẽ khác):
sshd[2401]: Authentication refused: bad ownership or modes for directory /home/deploy
Triệu chứng: bị từ chối dù khóa đúng. Nguyên nhân: sshd mặc định bật StrictModes, tức kiểm tra chủ sở hữu và quyền của home, .ssh và authorized_keys trước khi tin chúng, vì ai ghi được vào đó có thể tự thêm khóa. Cách sửa: sudo chmod 755 /home/deploy. Làm tương tự với authorized_keys (quyền 666) sẽ cho dòng log bad ownership or modes for file /home/deploy/.ssh/authorized_keys, sửa bằng chmod 600.
Lỗi 2, khóa private có quyền quá rộng ở client:
chmod 644 ~/.ssh/id_ed25519
ssh -o BatchMode=yes deploy@localhost true
chmod 600 ~/.ssh/id_ed25519
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: UNPROTECTED PRIVATE KEY FILE! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for '/home/you/.ssh/id_ed25519' are too open.
...
Load key "/home/you/.ssh/id_ed25519": bad permissions
deploy@localhost: Permission denied (publickey).
Lần này lý do nằm ngay trên client: SSH từ chối dùng khóa mà người khác đọc được. Hai lỗi này cho thấy bạn phải biết lỗi nằm ở phía nào trước khi đọc log.
Lỗi 3, thiếu authorized_keys, ví dụ xóa file đó, chỉ cho Permission denied, còn log chỉ có Connection closed by authenticating user deploy 127.0.0.1 port 60866 [preauth] và không có dòng "Authentication refused" nào, vì không có gì để từ chối. Dấu hiệu phân biệt: nếu log không nói gì về quyền, hãy kiểm tra file có tồn tại và chứa đúng public key không (sudo cat /home/deploy/.ssh/authorized_keys).
Lỗi 4, server dựng lại và host key đổi. Khi VM staging bị tạo mới, nó sinh host key mới và client in:
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Offending ECDSA key in /home/you/.ssh/known_hosts:3
remove with:
ssh-keygen -f '/home/you/.ssh/known_hosts' -R 'localhost'
Host key verification failed.
Đây là chiều tin tưởng thứ nhất hoạt động đúng. Nếu bạn vừa dựng lại VM thì nguyên nhân vô hại, xóa dòng cũ bằng lệnh ssh-keygen -R mà SSH gợi ý rồi kiểm tra lại fingerprint như phần đầu bài. Nếu bạn không dựng lại gì cả, đừng xóa gì, hãy điều tra. Trong CI, -o StrictHostKeyChecking=accept-new chấp nhận máy mới nhưng vẫn từ chối khi khóa của máy đã biết bị đổi, nên an toàn hơn StrictHostKeyChecking=no.
Siết cấu hình sshd mà không tự khóa mình
Ubuntu nạp mọi file *.conf trong /etc/ssh/sshd_config.d/ ở dòng Include đầu file sshd_config. Với phần lớn từ khóa, sshd dùng giá trị đầu tiên nó gặp, nên file có tên đứng trước theo thứ tự chữ cái sẽ thắng. Đặt tên 10-gdev.conf để chắc chắn đứng trước các file mặc định của distro hoặc cloud-init (thường bắt đầu bằng 50-):
sudo tee /etc/ssh/sshd_config.d/10-gdev.conf > /dev/null <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
MaxAuthTries 3
EOF
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|maxauthtries) '
Kết quả:
maxauthtries 3
permitrootlogin no
passwordauthentication no
kbdinteractiveauthentication no
sshd -T in ra cấu hình hiệu lực sau khi gộp mọi file, nên đây là cách chắc chắn nhất để biết giá trị thật, thay vì đọc từng file. Ba dòng đầu bịt đường tấn công đoán mật khẩu và root. KbdInteractiveAuthentication cũng cần tắt vì nó cho phép nhập mật khẩu qua một đường khác.
Quy trình áp dụng an toàn, theo đúng thứ tự:
- Giữ nguyên phiên SSH đang mở. Reload không làm rớt các phiên đã kết nối.
- Kiểm tra cú pháp:
sudo sshd -t. Một lỗi chính tả nhưPermitRootLognlàm sshd từ chối khởi động vớiBad configuration option, và nếu bạn restart thay vì kiểm tra trước thì mất đường vào. - Áp dụng:
sudo systemctl reload ssh. Đơn vịsshchạysshd -trồi gửi tín hiệu nạp lại. - Mở một phiên mới và đăng nhập thử. Chỉ đóng phiên cũ khi phiên mới vào được.
Cái bẫy kinh điển là AllowUsers. Thêm AllowUsers deploy vào file trên là hợp lý, nhưng nếu tài khoản bạn đang dùng để quản trị không có trong danh sách thì phiên mới bị từ chối:
sshd[3012]: User you from 127.0.0.1 not allowed because not listed in AllowUsers
Nhờ có phiên cũ còn mở, bạn sửa lại được. Đây là lý do quy trình bốn bước tồn tại. Một ghi chú về cổng: Ubuntu 24.04 chạy SSH theo kiểu socket activation. Nếu muốn đổi Port hay ListenAddress, ngoài sửa sshd_config còn phải sudo systemctl daemon-reload và sudo systemctl restart ssh.socket. Các tùy chọn khác như tắt mật khẩu chỉ cần reload ssh.
Cuối cùng, khóa cho pipeline. Khóa không có passphrase để CI dùng được thì không nên được phép làm mọi thứ. Trong authorized_keys, thêm tùy chọn trước khóa để ép chỉ chạy một lệnh:
restrict,command="/usr/local/bin/gdev-deploy" ssh-ed25519 AAAA... gdev-ci
restrict tắt chuyển tiếp cổng, agent và cấp terminal; command= ép mọi kết nối chỉ chạy lệnh đó, kể cả khi client yêu cầu lệnh khác. Trong lab, thay bằng command="echo deployed" rồi chạy ssh deploy@localhost 'rm -rf /' sẽ chỉ in deployed mà không xóa gì. Khi khóa của CI bị lộ, kẻ cầm nó cũng chỉ chạy được script deploy.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân hay gặp | Cách kiểm chứng |
|---|---|---|
Permission denied (publickey) dù khóa đúng | Quyền home, .ssh hoặc authorized_keys quá rộng; thiếu key; AllowUsers | sudo journalctl -u ssh -n 30, ls -ld ~ ~/.ssh trên server |
UNPROTECTED PRIVATE KEY FILE | Khóa private có quyền khác 600 | chmod 600 ~/.ssh/id_ed25519 |
REMOTE HOST IDENTIFICATION HAS CHANGED | VM dựng lại, đổi IP, hoặc bị chặn giữa đường | Đối chiếu fingerprint trước khi ssh-keygen -R |
| Tắt mật khẩu rồi mất truy cập | Key chưa cài, hoặc AllowUsers loại tài khoản | Giữ phiên cũ, sshd -t, thử phiên mới rồi mới đóng phiên cũ |
Sửa Port nhưng không đổi cổng trên Ubuntu 24.04 | Quên reload ssh.socket | sudo systemctl daemon-reload && sudo systemctl restart ssh.socket |
| Pipeline treo chờ nhập | Client hỏi xác nhận host key hoặc passphrase | Thêm -o BatchMode=yes để lỗi thay vì chờ |
| Bastion bị chiếm làm lộ khóa | Dùng ForwardAgent yes | Dùng ProxyJump |
Tự kiểm tra
Hãy trả lời trước khi đọc đáp án ngay bên dưới mỗi câu.
Câu 1. Client báo Permission denied (publickey). Bạn có một bản copy của authorized_keys và khóa đúng. Bạn mở file log nào, đọc dòng nào đầu tiên?
Đáp án: Log của sshd trên server, bằng sudo journalctl -u ssh. Tìm các dòng Authentication refused: bad ownership or modes hoặc not allowed because not listed in AllowUsers quanh thời điểm đăng nhập.
Câu 2. Vì sao StrictModes từ chối khi thư mục home có quyền 777?
Đáp án: Ai ghi được vào home có thể thay .ssh/authorized_keys bằng khóa của mình và đăng nhập thành tài khoản đó, nên sshd không tin file trong thư mục đó.
Câu 3. Bạn thêm AllowUsers deploy vào drop-in rồi reload ssh. Vì sao phải giữ phiên cũ mở trước khi đóng?
Đáp án: Nếu tài khoản quản trị của bạn không có trong danh sách, phiên mới sẽ bị từ chối. Phiên đã kết nối vẫn sống qua reload, nên bạn sửa được. Nếu đã đóng thì phải vào bằng console của nhà cung cấp.
Nếu chỉ nhớ 3 điều
- SSH tin cậy hai chiều: host key trong
known_hostscho server, public key trongauthorized_keyscho client, và private key không rời máy. - Client chỉ thấy
Permission denied; lý do thật nằm ởjournalctl -u sshtrên server, thường là quyền file hoặcAllowUsers. - Siết cấu hình theo thứ tự: giữ phiên cũ,
sshd -t,reload ssh, thử phiên mới rồi mới đóng phiên cũ.
Bài tiếp theo là Quản lý file trên Linux: tổ chức thư mục triển khai và xử lý khi đĩa đầy.
Tài liệu chính thức: sshd_config, ssh_config, ssh-keygen, OpenSSH trên Ubuntu Server và SSH socket activation trên Ubuntu.