Guide
How to use SFTP in GitHub Actions
Upload build artifacts or deploy files over SFTP from a GitHub Actions workflow, using an SSH key and host key stored in secrets. Includes a complete, secure workflow YAML.
Shipping a build to an SFTP destination from CI is common: publish a static site, drop release artifacts to a partner, push a data export. In GitHub Actions the key things to get right are secret handling (never hard-code a key) and host-key verification (so CI isn’t trusting an unknown server). Here’s a complete, secure setup.
Step 1, store three secrets
In your repo: Settings → Secrets and variables → Actions, add:
SFTP_KEY, the private SSH key for a dedicated CI user (generate a fresh one; don’t reuse a personal key).SFTP_HOST, the hostname, e.g.sftp.example.com.SFTP_KNOWN_HOSTS, the server’s known_hosts line (fromssh-keyscan -t ed25519 sftp.example.com), so the connection is verified.
Register the matching public key with your SFTP user in the provider’s console.
Step 2, the workflow
name: Deploy over SFTP
on:
push:
branches: [main]
jobs:
upload:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: npm ci && npm run build # produces ./dist
- name: Configure SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SFTP_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
echo "${{ secrets.SFTP_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
- name: Upload via SFTP
run: |
sftp -i ~/.ssh/id_ed25519 ci@${{ secrets.SFTP_HOST }} <<'EOF'
put -r ./dist /incoming/
bye
EOF
The heredoc feeds batch commands to sftp non-interactively. For many files with retries,
swap the upload step for lftp:
- name: Upload via lftp
run: |
sudo apt-get update && sudo apt-get install -y lftp
lftp -u ci, -e "mirror -R ./dist /incoming; bye" \
sftp://${{ secrets.SFTP_HOST }}
Security notes
- Dedicated CI credential. Issue one SFTP user just for Actions, path-jailed to the destination prefix, so a leaked key can’t reach anything else, and you can revoke it without affecting other automations.
- Never echo secrets. Don’t
catthe key or run withset -xaround it. - Verify the host key (the
SFTP_KNOWN_HOSTSsecret). Skipping it, or usingStrictHostKeyChecking=no, means CI will happily upload to an impostor. - Source-IP pinning is awkward here, GitHub’s runners use a wide, changing IP range, so rely on key auth and path jails rather than IP allowlisting for hosted runners.
Where the files go
With a bring-your-own-bucket gateway, the workflow uploads straight into your own S3, Azure, or GCS bucket, with the transfer recorded in the audit trail. See SFTP to S3, or connect to SFTP using Python if you’d rather script it in a language step.
Try it on your own bucket
Connect a bucket you already own, Amazon S3, Azure Blob, Google Cloud Storage, or an S3-compatible store, and hand out a clean SFTP endpoint in minutes. Your files stay in your cloud.
Start free