Tìm hiểu nhanh SFTP và Paramiko python

Hi Vtitans,
Mình nhận bài tập lấy file từ SFTP on-premises và đưa lên AWS S3, cũng đang vừa tìm hiểu cách làm vừa blog. Trong bài này mình ghi lại các bước practice để hiểu sơ lược cách code python dùng paramiko.

Cài đặt SFTP

Mình dùng AMI Ubuntu server 20. Requirement bao gồm:

  • có public IP
  • mở port 22
  • cho phép SSH bằng user ubuntu và private key

Quá trình cài đặt tóm tắt là: trong trường hợp này, SFTP không phải là một gói cài riêng mà là sub-system có sẵn trong sshd của Ubuntu 20. Các step bao gồm:

  • Tạo một user có password
  • Tạo thư mục data và phân quyền
  • Configure sshd và restart service

Chi tiết có trong https://qiita.com/alokrawat050/items/709d3c777407ab658aa9
Ở đây chỉ ghi lại config để đoạn sau dễ trình bày:

# /etc/ssh/sshd_config
# blah blah
Port 22
Match User sftp_user
ForceCommand internal-sftp
PasswordAuthentication yes
ChrootDirectory /var/sftp/myfolder
PermitTunnel no
AllowAgentForwarding no
AllowTcpForwarding no
X11Forwarding no

Tạo file để test: Trong guide trên đã có đoạn tạo thư mục /var/sftp/myfolder/data với owner là sftp_user.

sudo su - sftp_user
echo "A,1" >/var/sftp/myfolder/data/test.csv

Bây giờ từ máy dev, bạn kiểm tra cài đặt thành công bằng cách download file về nhé:

sftp sftp_user@ip:/data/test.csv .

Chỗ này hãy để ý đường dẫn file. Trong code python bạn cũng sẽ dùng đường dẫn này để lấy file chứ không phải là /var/sftp/myfolder/data/test.csv nha.
Đến đây ta dừng lại để, như mình hay tự nói với bản thân, remove confusion!

SFTP vs SSH

Ở trên bạn thấy mình không "cài" sftp, mà chỉ config nó trong sshd. Tiếp theo bạn lại thấy mình dùng lệnh sftp để get file thay vì lệnh scp quen thuộc. Vậy sftp và ssh phân biệt như thế nào?
Xin trích dẫn: "SFTP không thể thiếu SSH - nó dùng SSH để transfer file. Nói cách khác, SSH protocol được dùng trong SFTP file transfer mechanism. Phần lớn SSH servers trong thực tế có sẵn SFTP. Ngược lại, không phải tất cả SFTP servers đều hỗ trợ SSH commands 'n' actions."
Và những câu hỏi ban đầu nảy sinh trong đầu mình, hầu như được giải đáp tại link này, nên cũng tạm hài lòng: https://www.goanywhere.com/blog/are-ssh-and-sftp-the-same

SFTP vs FTP/FTPS

Vâng ta dùng một thư viện hay tool http thì thấy nó làm việc với cả http lẫn https một cách khá là vô tư. Nhưng ở đây tình huống lại khác. Mình không kiên trì lục lọi kinh viện để chém gió đâu, là một dev cục súc, mình kết luận luôn là để support cả 2 thì effort gấp đôi và đừng wrap 2 thứ khác hẳn nhau làm chi.

  1. Các tool như WinSCP sẽ support nhiều giao thức, nhưng đừng nhầm lẫn, ta đang tìm một thư viện, hay chính xác là tìm cách code giống nhau, optimize và handle error giống nhau cho cả 2 thì không có.
  2. Đáng chú ý với anh em đá sân infra: SFTP cố định port, FTP có thể nhảy port (sau khi client đã bắt tay làm quen với server ở một port cố định, chúng sẽ phi data cho nhau ở các port động => quả của cái nhân này là phải mở bung firewall).

Paramiko

pysftp dùng paramiko, cả hai, hơi ngạc nhiên, không có mặt trong AWS Lambda python runtime, nhưng nếu mình không dùng Lambda cho bài toán này thì do những ràng buộc khác.
Nào code vài dòng nhé:

import paramiko # pip3 install paramiko
#
host = 'ip.192.ip.70'
port = 22
username = 'sftp_user'
password = 'hỏi chủ quán'
#
def create_client(host, port, username, password):
    transport = paramiko.Transport(host, port)
    transport.connect(username=username, password=password)
    client = paramiko.SFTPClient.from_transport(transport)
    return client
#
if __name__ == '__main__':
    client = create_client(host, port, username, password)
    s = client.listdir()
    print(s)

paramiko cung cấp 2 client: SSHClient và SFTPClient nên lúc code đừng nhầm nhé. Mình lược try catch đi để show thủ tục bắt tay: Transport -> Connect -> Create client

Nếu muốn get file, như đã lưu ý đường dẫn có thể là /data/test.csv hoặc data/test.csv, nhưng không phải là /var/sftp/myfolder/data/test.csv:

remote_file = client.file('/data/test.csv', 'r')
data = remote_file.read()
remote_file.close()

Giờ trở lại đoạn code thủ tục phía trên, không có gì để nói thêm trừ một điều: ồ check host key đâu?
Mình đoánnếu bạn gặp lỗi Server Unknown rồi đi search thì sẽ gặp kha khá mớ code và giải thích đủ làm nồi lẩu, nên ở đây ta lại dừng một chút để, remove confusion.

Verify host key

They said, nếu như bạn code tự động add (nói cách khác, trust) một host lạ (unknown) thì bạn đối mặt với MITM attack. Mình gặp nhiều chú cứ nói đến security với hack là phấn khích quái dị, còn mình thì hem biết hihi.
Nếu bạn lần đầu ssh, scp hay sftp đến một IP a.b.c.d, lệnh đó sẽ hỏi bạn có permanently add cái host đó không. Đồng ý => nó sẽ add một dòng gọi là "host key" vào file, thường là ~/.ssh/known_hosts:

a.b.c.d ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOVTHav/KTDtLqIb/f8wtkEbU71sZiv9w2yGhO0mLfcED9Mt0sQrIFRsZqFudio09vuuQR7Ya69H1RplZqabaMU=

Thông tin này do server trả về. Lần sau, key đã có. Nếu server trả về cùng thông tin thì lệnh không hỏi gì nữa, nếu server trả về khác, lệnh nghi ngại và lại hỏi bạn có trust không. Bạn có thể thay đổi hành xử của lệnh bằng options, nhưng chi tiết đó không quan trọng.

Câu hỏi ở đây là, như vậy có gọi là Verify không ta? Người dùng toàn yes hết chứ có kiểm tra cái gì đâu?
Việc verify, nếu một công ty triển khai đầy đủ cho các client của họ thì sơ lược là:

  • Admin lấy key của server xịn xò gửi cho end user
  • End user paste nó vào known_hosts

Để lấy key khi bạn là Admin, do bạn có private key, bạn đơn giản là gen ra pub key. Trong trường hợp cài đặt để test giống hệt mình, bạn có thể check:

sudo cat /etc/ssh/ssh_host_ecdsa_key.pub 
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOVTHav/KTDtLqIb/f8wtkEbU71sZiv9w2yGhO0mLfcED9Mt0sQrIFRsZqFudio09vuuQR7Ya69H1RplZqabaMU= root@ip-172-31-0-155

Mình cũng nghi ngờ bản thân và các từ mình viết ra, bạn có thể tham khảo thêm, ví dụ: https://docs.bmc.com/docs/display/itda27/About+the+SSH+host+key+fingerprint

Câu chuyện cũng tương tự với WinSCP, FileZilla và Paramiko. Mình thấy rằng, việc verify host key là compare nó với một key đã biết, do vậy Paramiko trước hết phải biết lấy key đó ở đâu - và ở đây có một số hàm liên quan mà bạn sẽ có trong nồi lẩu search ra lúc trước:

  1. load_system_host_keys
  2. load_host_keys

Và khi một key là lạ (missing == unknown), Paramiko cho phép bạn code để chọn Policy, cơ bản là: Reject (default) hoặc Add (accept). Và khi bạn chọn Add, đó chính là lúc có anh chàng reo lên là bạn mở cửa cho MITM attack đó =)))

Nếu code của bạn chạy serverless (nơi mà khái niệm local persistence cứ phải giải thích lại hoài), khả năng của bạn để có key mà check là: lấy host key từ remote storage, hoặc đóng gói known_hosts cùng source tức là hard code, hoặc passing nó như parameter. Đều dẫn đến sự phức tạp về dev và vận hành khi bạn có hàng nghìn con server trong data center và chúng lại còn thay đổi, đồng nghĩa chi phí cao hơn.

Trở lại với đoạn code trên. Mình đã cẩn thận (thừa, vì mình đâu có load keys) xoá file known_hosts và test lại, vẫn chạy. Câu trả lời có trong api doc của Paramiko, cụ thể là hàm Transport.connect().
Vậy, mình gửi link đây, hi vọng bạn sẽ tò mò mà đọc nó nhé: http://docs.paramiko.org/en/stable/api/transport.html

Chúc dự án của bạn thành công,
Thân, from Châu D9

Leave a Reply

Your email address will not be published. Required fields are marked *