<?xml version="1.0" encoding="utf-8"?>
<!-- 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
-->
<?xml-stylesheet type="text/xsl" href="https://rollerweblogger.org/roller-ui/styles/rss.xsl" media="screen"?><rss version="2.0" 
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns:atom="http://www.w3.org/2005/Atom" >
<channel>
  <title>Blogging Roller</title>
  <link>https://rollerweblogger.org/roller/</link>
    <atom:link rel="self" type="application/rss+xml" href="https://rollerweblogger.org/roller/feed/entries/rss?tags=actions" />
  <description>Dave Johnson on open web technologies, social software and software development</description>
  <language>en-us</language>
  <copyright>Copyright 2026</copyright>
  <lastBuildDate>Wed, 3 Jun 2026 02:49:03 +0000</lastBuildDate>
  <generator>Apache Roller 6.1.5</generator>
  <item>
    <guid isPermaLink="true">https://rollerweblogger.org/roller/entry/github-actions-and-digitalocean-lessons</guid>
    <title>GitHub Actions and DigitalOcean lessons from Claude</title>
    <dc:creator>Dave Johnson</dc:creator>
    <link>https://rollerweblogger.org/roller/entry/github-actions-and-digitalocean-lessons</link>
    <pubDate>Tue, 2 Jun 2026 21:30:00 +0000</pubDate>
    <category>Web Development</category>
    <category>actions</category>
    <category>cd</category>
    <category>ci</category>
    <category>claude</category>
    <category>code</category>
    <category>digitalocean</category>
    <category>github</category>
    <category>llms</category>
    <category>security</category>
    <category>terraform</category>
<atom:summary type="html">You might find this interesting if you use LLMs to generate code or GitHub Actions to deploy to DigitalOcean.</atom:summary><description>&lt;p&gt;I would say that 99% of the code in the&amp;nbsp;&lt;a href=&quot;https://snoopdavellc.com/investorping&quot; target=&quot;_blank&quot;&gt;Investor Ping&lt;/a&gt;&amp;#39;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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;So, I worked with Claude Code to create a comprehensive &lt;strong&gt;security review&lt;/strong&gt; 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.&lt;/p&gt;
&lt;h5&gt;&lt;br&gt;&lt;/h5&gt;&lt;h5&gt;📍 Pin third-party GitHub Actions to a commit SHA&lt;/h5&gt;
&lt;p&gt;The &lt;strong&gt;first&lt;/strong&gt; 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.&lt;/p&gt;
&lt;pre&gt;# Before — tag reference, mutable
- uses: actions/checkout@v4

# After — immutable SHA, version tag in comment for humans
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2&lt;/pre&gt;
&lt;h5&gt;&lt;br&gt;&lt;/h5&gt;&lt;h5&gt;🔐 Lock-down SSH with StrictHostKeyChecking=yes and ForceCommand&lt;/h5&gt;
&lt;p&gt;The &lt;strong&gt;second&lt;/strong&gt; item is about how CI/CD accesses the production servers via SSH using a 3rd party GitHub Action and &lt;code&gt;StrictHostKeyChecking=no&lt;/code&gt;. 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&amp;#39;t want CI/CD to have the keys to the kingdom.&lt;/p&gt;
&lt;p&gt;The fix is to add &lt;code&gt;StrictHostKeyChecking=yes&lt;/code&gt; and use an SSH feature called &lt;code&gt;ForceCommand&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;On the host, restrict the CI key in &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt;&amp;nbsp;to just the deploy script:&lt;/p&gt;
&lt;pre&gt;command=&amp;quot;/usr/local/bin/deploy.sh&amp;quot;,no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-ed25519 AAAA...ci-key... ci-deploy@github&lt;/pre&gt;
&lt;p&gt;In the workflow, drop the third-party SSH action and pin the host key:&lt;/p&gt;
&lt;pre&gt;- 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 &amp;#39;%s\n&amp;#39; &amp;quot;$SSH_KEY&amp;quot;     &amp;gt; ~/.ssh/id_ed25519  &amp;amp;&amp;amp; chmod 600 ~/.ssh/id_ed25519
    printf &amp;#39;%s\n&amp;#39; &amp;quot;$KNOWN_HOSTS&amp;quot; &amp;gt; ~/.ssh/known_hosts &amp;amp;&amp;amp; chmod 600 ~/.ssh/known_hosts
    ssh -o StrictHostKeyChecking=yes -i ~/.ssh/id_ed25519 \
        deploy@&amp;quot;$DEPLOY_HOST&amp;quot; deploy&lt;/pre&gt;
&lt;h5&gt;&lt;br&gt;&lt;/h5&gt;&lt;h5&gt;🪖 Use least-privilege DigitalOcean access keys&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Third&lt;/strong&gt;, CI/CD runs Terraform with an all-powerful DigitalOcean API access token when some workflows only need a couple of permissions.&lt;/p&gt;
&lt;p&gt;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:&lt;/p&gt;
&lt;pre&gt;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&lt;/pre&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;p&gt;The &lt;strong&gt;fourth&lt;/strong&gt; and &lt;strong&gt;fifth&lt;/strong&gt; items were NPM dependencies with critical vulnerabilities that need an update. I feel like I need to learn some lessons in this area.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;The overall lesson: rapidly producing code with LLMs leads to security and other problems, because try as you may, you can&amp;#39;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.&lt;/p&gt;
</description>  </item>
</channel>
</rss>