FTP Deploy with GitHub Actions
This article describes how I use GitHub Actions to deploy content using FTP without any third-party dependencies. Code executed in continuous deployment pipelines may have access to secrets (like FTP credentials and SSH keys). Supply-chain attacks are becoming more frequent, including self-sabotage by open-source authors. Without 2FA, the code of well-intentioned maintainers is one stolen password away from becoming malicious. For these reasons I find it imperative to eliminate third-party Actions from my CI/CD pipelines wherever possible.
⚠️ WARNING: Third-party Actions in the GitHub Actions Marketplace may be compromised to run malicious code and leak secrets. There are dozens of public actions claiming to facilitate FTP deployment. I advise avoiding third-party actions in your CI/CD pipeline whenever possible.
This article assumes you have at least some familiarity with GitHub Actions, but if you’re never used them before I recommend taking 5 minutes to work through the Quickstart for GitHub Actions.
FTP Deployment Workflow
This workflow demonstrates how to use LFTP inside a GitHub Action to transfer files/folders with FTP without requiring a third-party dependency. Users can copy/paste this workflow and edit it as needed according to the LFTP manual.
name: 🚀 FTP Deploy
on: [push, workflow_dispatch]
jobs:
ftp-deploy:
runs-on: ubuntu-latest
steps:
- name: 🛒 Checkout
uses: actions/checkout@v2
- name: 📦 Get LFTP
run: sudo apt install lftp
- name: 🛠️ Configure LFTP
run: mkdir ~/.lftp && echo "set ssl:verify-certificate false;" >> ~/.lftp/rc
- name: 🔑 Load Secrets
run: echo "machine ${{ secrets.FTP_HOSTNAME }} login ${{ secrets.FTP_USERNAME }} password ${{ secrets.FTP_PASSWORD }}" > ~/.netrc
- name: 📄 Upload File
run: lftp -e "put -O /destination/ ./README.md" ${{ secrets.FTP_HOSTNAME }}
- name: 📁 Upload Folder
run: lftp -e "mirror --parallel=100 -R ./ffmpeg/ /ffmpeg/" ${{ secrets.FTP_HOSTNAME }}
This workflow uses GitHub Encrypted Secrets to store secret values:
FTP_HOSTNAME
- a string likeftp.example.com
FTP_USERNAME
- a string likelogin@example.com
FTP_PASSWORD
- a string likesuperSecret123

How to Verify the Host Certificate
Extra steps can be taken to record the host’s public certificate, store it as a GitHub Encrypted Secret, load it into the GitHub Action runner, and configure LFTP to compare against at run time.
- 1: Acquire your host’s entire certificate chain. The
-showcerts
argument was critically important for me.
openssl s_client -connect example.com:21 -starttls ftp -showcerts
-
2: Copy the entire output, convert it to a Base64 string, and store it as a GitHub Encrypted Secret named
FTP_CERTS_BASE64
-
3: Update your GitHub Action to save the certificate file and configure LFTP to use it:
- name: 🛠️ Configure LFTP
run: |
mkdir ~/.lftp
echo "set ssl:ca-file ~/.lftp/certs.crt;set ssl:check-hostname no;" >> ~/.lftp/rc
echo "${{ secrets.FTP_CERTS_BASE64 }}" | base64 --decode > ~/.lftp/certs.crt
Notes
To avoid storing passwords to disk you can pass them in with each lftp
command using the -u
argument. See the LFTP Documentation for details.
Although potentially insecure, some GitHub Marketplace Actions offer compelling features: One of the most popular is SamKirkland’s FTP Deploy Action which has advanced features like the use of server-stored JSON files to store file hashes to detect and selectively re-upload changed files. I encourage you to check them out, even though I try to avoid passing my secrets through third-party actions wherever possible.
Favor SSH and rsync
over FTP and lftp
where possible because rsync
is faster, more secure, and designed to prevent needless transfer of unchanged files. I recently wrote about how to safely deploy over SSH using rsync with GitHub Actions.