Blogging Roller

Dave Johnson on open web technologies, social software and software development

GitHub Actions and DigitalOcean lessons from Claude

I would say that 99% of the code in the Investor Ping's backend and iOS app was written by LLMs, but I did not vibe code it. My definition of vibe coding is when you use an LLM to write code that you do not review or even read. Nope, I ran the project like I always do with design docs, issue tracking, pull requests and CI/CD sandbox and production deploys (in this case to DigitalOcean). I review all code and try to enforce some architectural constraints, mostly about separation of concerns, configuration and secrets management; but maybe less so in the iOS app because I am new to iOS and Swift.

Despite that, I ended up with some poor security practices baked into my codebase. I knew that would happen given the volume of code I was generating and my desire to move fast.

So, I worked with Claude Code to create a comprehensive security review and Claude found eighteen issues that should be fixed; most were in GitHub Actions workflows and secrets; the rest were vulnerable NPM dependencies and application level problems. The review created is an impressive document that explains each problem and how to fix it. Claude made a list of the top five things to fix and three of them were good lessons to learn.


📍 Pin third-party GitHub Actions to a commit SHA

The first item is to pin the third-party GitHub Actions used in my workflows to specific commit SHAs to reduce the chance of a supply chain attack. This seems reasonable and easy to do.

# Before — tag reference, mutable
- uses: actions/checkout@v4

# After — immutable SHA, version tag in comment for humans
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

🔐 Lock-down SSH with StrictHostKeyChecking=yes and ForceCommand

The second item is about how CI/CD accesses the production servers via SSH using a 3rd party GitHub Action and StrictHostKeyChecking=no. The workflow uses SSH to do deployment: to write config and secrets to a Docker Compose file and start up my containers. I think this is the most serious issue found. We don't want CI/CD to have the keys to the kingdom.

The fix is to add StrictHostKeyChecking=yes and use an SSH feature called ForceCommand to lock the SSH key down so that it can only run one specific script on the remote host, and to get the secrets to the hosts via a diferent route: a script I run on my laptop and a different SSH key.

On the host, restrict the CI key in ~/.ssh/authorized_keys to just the deploy script:

command="/usr/local/bin/deploy.sh",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-ed25519 AAAA...ci-key... ci-deploy@github

In the workflow, drop the third-party SSH action and pin the host key:

- name: Deploy
  env:
    DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
    SSH_KEY:     ${{ secrets.DEPLOY_SSH_KEY }}
    KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
  run: |
    install -d -m 700 ~/.ssh
    printf '%s\n' "$SSH_KEY"     > ~/.ssh/id_ed25519  && chmod 600 ~/.ssh/id_ed25519
    printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts && chmod 600 ~/.ssh/known_hosts
    ssh -o StrictHostKeyChecking=yes -i ~/.ssh/id_ed25519 \
        deploy@"$DEPLOY_HOST" deploy

🪖 Use least-privilege DigitalOcean access keys

Third, CI/CD runs Terraform with an all-powerful DigitalOcean API access token when some workflows only need a couple of permissions.

DO supports custom-scoped tokens (Control Panel → API → Generate New Token → Custom Scopes). Create one per workflow, stored as a separate GitHub secret, e.g. a token just for registry management:

jobs:
  tf-registry:
    environment: prod              # required reviewer before apply
    env:
      # token scopes: registry:read, registry:create, registry:delete
      DIGITALOCEAN_TOKEN: ${{ secrets.DO_TOKEN_REGISTRY }}
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: hashicorp/setup-terraform@b9cd47afb6f8aabd99b34a4673bf25b95f6d4d0d # v3.1.2
      - run: terraform -chdir=terraform/registry apply -auto-approve


The fourth and fifth items were NPM dependencies with critical vulnerabilities that need an update. I feel like I need to learn some lessons in this area.

It took me maybe three Claude Code sessions, a couple of afternoons, to deploy fixes for those five problems plus two others. In the end it was nine pull requests and ~2,200 added / ~630 deleted lines of code.

The overall lesson: rapidly producing code with LLMs leads to security and other problems, because try as you may, you can't review every line. But a great LLM harness like Claude Code can search and probe your codebase from every angle and help you find and fix those problems quickly.

Dave Johnson in Web Development • 🕒 05:30PM Jun 02, 2026
Comments:

Post a Comment:
  • HTML Syntax: NOT allowed