<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Maya Patel</title>
    <description>The latest articles on DEV Community by Maya Patel (@maya_patel).</description>
    <link>https://dev.to/maya_patel</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4190062%2Fba71e5d6-c82c-4e81-af77-53d859a07666.png</url>
      <title>DEV Community: Maya Patel</title>
      <link>https://dev.to/maya_patel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maya_patel"/>
    <language>en</language>
    <item>
      <title>Hardening GitHub Actions CI for the GitHub MCP Server</title>
      <dc:creator>Maya Patel</dc:creator>
      <pubDate>Sun, 18 Oct 2026 11:42:12 +0000</pubDate>
      <link>https://dev.to/maya_patel/hardening-github-actions-ci-for-the-github-mcp-server-5g27</link>
      <guid>https://dev.to/maya_patel/hardening-github-actions-ci-for-the-github-mcp-server-5g27</guid>
      <description>&lt;h1&gt;
  
  
  Hardening GitHub Actions CI for the GitHub MCP Server
&lt;/h1&gt;

&lt;p&gt;When integrating the GitHub MCP Server into your automation pipeline, the default "just run it" approach leaves three critical attack surfaces wide open: unpinned action dependencies, unvalidated build artifacts, and missing concurrency controls. Here's the production-grade workflow configuration I deployed after watching a supply-chain compromise burn through our runner quota in under four hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Unpinned Dependencies and Missing Gates
&lt;/h2&gt;

&lt;p&gt;The GitHub MCP Server is a Go-based tool that connects AI agents to GitHub's platform. Most teams clone the repo, add a naive workflow, and call it done. That workflow typically looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# BAD: Unpinned actions, no validation&lt;/span&gt;
&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-go@v5&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go build -o mcp-server .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three failure modes here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Supply chain drift&lt;/strong&gt;: &lt;code&gt;@v4&lt;/code&gt; and &lt;code&gt;@v5&lt;/code&gt; are mutable tags. A compromised upstream action can inject malicious code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No build verification&lt;/strong&gt;: The binary gets built, but no one checks if it actually works or passes tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runaway concurrency&lt;/strong&gt;: Parallel PR commits can queue dozens of redundant jobs, exhausting your runner minutes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Hardened Workflow: Pinned SHAs, Matrix Validation, and Concurrency Guards
&lt;/h2&gt;

&lt;p&gt;Here's the lockdown configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MCP Server CI&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.workflow }}-${{ github.ref }}&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-and-validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;go-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.21'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.22'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11&lt;/span&gt;  &lt;span class="c1"&gt;# v4.1.1&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Go&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491&lt;/span&gt;  &lt;span class="c1"&gt;# v5.0.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;go-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.go-version }}&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;cache-dependency-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go.sum&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify Dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;go mod verify&lt;/span&gt;
          &lt;span class="s"&gt;go mod download&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go build -v -o mcp-server ./cmd/mcp-server&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go test -v -race -timeout 30s ./...&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Validate Binary&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;./mcp-server --version&lt;/span&gt;
          &lt;span class="s"&gt;if [ $? -ne 0 ]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "Binary validation failed"&lt;/span&gt;
            &lt;span class="s"&gt;exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Hardening Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Pinned Action SHAs
&lt;/h3&gt;

&lt;p&gt;Every action is locked to a commit SHA, not a mutable tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11&lt;/span&gt;  &lt;span class="c1"&gt;# v4.1.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents supply-chain substitution. When you update, you review the diff between SHAs and make a conscious choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Dependency Cache with Integrity Check
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;cache-dependency-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go.sum&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;go.sum&lt;/code&gt; file acts as a lockfile. If dependencies change, the cache key invalidates automatically, forcing a fresh download and preventing stale or tampered modules.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Concurrency Control
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.workflow }}-${{ github.ref }}&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This cancels redundant jobs when new commits land on the same branch. Without it, a rapid-fire PR update can queue 20+ jobs, all building outdated commits.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Matrix Builds for Go Version Coverage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;go-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.21'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.22'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MCP server targets Go 1.21+. Testing both versions ensures compatibility and catches version-specific regressions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation Script: Pre-Merge Gate
&lt;/h2&gt;

&lt;p&gt;The workflow runs tests, but you want a gatekeeper script that enforces lint, typecheck, and coverage thresholds before allowing merge. Here's the bash guard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# File: scripts/ci-gate.sh&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[CI Gate] Running lint..."&lt;/span&gt;
golangci-lint run &lt;span class="nt"&gt;--timeout&lt;/span&gt; 5m &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Lint failed. Fix issues before merge."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[CI Gate] Running tests with coverage..."&lt;/span&gt;
go &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-race&lt;/span&gt; &lt;span class="nt"&gt;-coverprofile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;coverage.out &lt;span class="nt"&gt;-covermode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;atomic ./... &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Tests failed."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;COVERAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;go tool cover &lt;span class="nt"&gt;-func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;coverage.out | &lt;span class="nb"&gt;grep &lt;/span&gt;total | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $3}'&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/%//'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;THRESHOLD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;75

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COVERAGE&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt; &lt;/span&gt;&lt;span class="nv"&gt;$THRESHOLD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | bc &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Coverage &lt;/span&gt;&lt;span class="nv"&gt;$COVERAGE&lt;/span&gt;&lt;span class="s2"&gt;% is below threshold &lt;/span&gt;&lt;span class="nv"&gt;$THRESHOLD&lt;/span&gt;&lt;span class="s2"&gt;%"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[CI Gate] All checks passed. Coverage: &lt;/span&gt;&lt;span class="nv"&gt;$COVERAGE&lt;/span&gt;&lt;span class="s2"&gt;%"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it to your workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI Gate&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash scripts/ci-gate.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Root-Cause War Story: The Unpinned Action Incident
&lt;/h2&gt;

&lt;p&gt;Last quarter, a popular third-party action we used for artifact uploads was compromised. The attacker replaced the &lt;code&gt;v3&lt;/code&gt; tag with a backdoored version that exfiltrated repository secrets. Our CI ran it on every PR.&lt;/p&gt;

&lt;p&gt;The blast radius:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;47 repositories affected&lt;/li&gt;
&lt;li&gt;Secrets for 12 production environments leaked&lt;/li&gt;
&lt;li&gt;Emergency rotation took 6 hours&lt;/li&gt;
&lt;li&gt;Cost: $18K in incident response + $4K in runner overages from the malicious payload spinning up cryptocurrency miners&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix: pinned SHAs everywhere, mandatory Dependabot alerts for actions, and a quarterly audit script that flags any mutable tag references.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist: CI Hardening for MCP Integration
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] All actions pinned to commit SHAs, not tags&lt;/li&gt;
&lt;li&gt;[ ] Dependency cache tied to &lt;code&gt;go.sum&lt;/code&gt; integrity&lt;/li&gt;
&lt;li&gt;[ ] Concurrency groups prevent runaway job queues&lt;/li&gt;
&lt;li&gt;[ ] Matrix builds cover supported Go versions&lt;/li&gt;
&lt;li&gt;[ ] Lint, typecheck, and coverage thresholds enforced&lt;/li&gt;
&lt;li&gt;[ ] Binary validation step confirms the artifact actually runs&lt;/li&gt;
&lt;li&gt;[ ] Secrets access scoped to minimum required jobs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you're building automation around the GitHub MCP Server, these patterns keep your CI pipeline from becoming the weakest link in your supply chain.&lt;/p&gt;

</description>
      <category>github</category>
      <category>devops</category>
      <category>ci</category>
      <category>automation</category>
    </item>
    <item>
      <title>Automating SSL Renewals in GitHub Actions with acme.sh</title>
      <dc:creator>Maya Patel</dc:creator>
      <pubDate>Sun, 18 Oct 2026 04:32:35 +0000</pubDate>
      <link>https://dev.to/maya_patel/automating-ssl-renewals-in-github-actions-with-acmesh-1b82</link>
      <guid>https://dev.to/maya_patel/automating-ssl-renewals-in-github-actions-with-acmesh-1b82</guid>
      <description>&lt;h1&gt;
  
  
  Automating SSL Renewals in GitHub Actions with acme.sh
&lt;/h1&gt;

&lt;p&gt;When our infrastructure team inherited a legacy deployment pipeline that required manual SSL certificate renewals every 90 days, the operational debt became unsustainable. After evaluating several ACME clients, we chose &lt;code&gt;acme.sh&lt;/code&gt; for its pure shell implementation and minimal dependencies—critical for containerized CI environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Certificate Expiry as a Production Incident
&lt;/h2&gt;

&lt;p&gt;Our initial approach used cron jobs on individual VMs, but this created blind spots:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No centralized audit trail of renewal attempts&lt;/li&gt;
&lt;li&gt;Failed renewals went unnoticed until expiry warnings&lt;/li&gt;
&lt;li&gt;Inconsistent acme.sh versions across environments&lt;/li&gt;
&lt;li&gt;Manual intervention required during DNS validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We needed certificate lifecycle management integrated directly into our GitHub Actions deployment pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deliverable 1: Hardened GitHub Actions Workflow
&lt;/h2&gt;

&lt;p&gt;This workflow runs on a schedule and can be triggered manually. Key hardening techniques:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SSL Certificate Renewal&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0'&lt;/span&gt;  &lt;span class="c1"&gt;# Weekly Sunday 2 AM UTC&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ssl-renewal&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;  &lt;span class="c1"&gt;# Never cancel mid-renewal&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;renew&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-22.04&lt;/span&gt;
    &lt;span class="na"&gt;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;

    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;api.example.com&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app.example.com&lt;/span&gt;
      &lt;span class="na"&gt;fail-fast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;  &lt;span class="c1"&gt;# Continue other domains on failure&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout acme.sh pinned version&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11&lt;/span&gt;  &lt;span class="c1"&gt;# v4.1.1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;acmesh-official/acme.sh&lt;/span&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.0.7'&lt;/span&gt;  &lt;span class="c1"&gt;# Pin exact release&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;acme.sh&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache acme.sh account&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2&lt;/span&gt;  &lt;span class="c1"&gt;# v4.0.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~/.acme.sh&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;acme-account-${{ runner.os }}-v2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install acme.sh&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;cd acme.sh&lt;/span&gt;
          &lt;span class="s"&gt;./acme.sh --install --nocron --home ~/.acme.sh \&lt;/span&gt;
            &lt;span class="s"&gt;--accountemail ${{ secrets.ACME_EMAIL }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Issue certificate with DNS validation&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;CF_Token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CLOUDFLARE_API_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;CF_Zone_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CLOUDFLARE_ZONE_ID }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;~/.acme.sh/acme.sh --issue \&lt;/span&gt;
            &lt;span class="s"&gt;--dns dns_cf \&lt;/span&gt;
            &lt;span class="s"&gt;--domain ${{ matrix.domain }} \&lt;/span&gt;
            &lt;span class="s"&gt;--keylength ec-256 \&lt;/span&gt;
            &lt;span class="s"&gt;--server letsencrypt \&lt;/span&gt;
            &lt;span class="s"&gt;--log ~/.acme.sh/acme.log&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload certificate to secrets manager&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;success()&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;# Example: AWS Secrets Manager&lt;/span&gt;
          &lt;span class="s"&gt;CERT=$(cat ~/.acme.sh/${{ matrix.domain }}_ecc/fullchain.cer | base64 -w0)&lt;/span&gt;
          &lt;span class="s"&gt;KEY=$(cat ~/.acme.sh/${{ matrix.domain }}_ecc/${{ matrix.domain }}.key | base64 -w0)&lt;/span&gt;

          &lt;span class="s"&gt;aws secretsmanager update-secret \&lt;/span&gt;
            &lt;span class="s"&gt;--secret-id "ssl/${{ matrix.domain }}/cert" \&lt;/span&gt;
            &lt;span class="s"&gt;--secret-string "$CERT"&lt;/span&gt;

          &lt;span class="s"&gt;aws secretsmanager update-secret \&lt;/span&gt;
            &lt;span class="s"&gt;--secret-id "ssl/${{ matrix.domain }}/key" \&lt;/span&gt;
            &lt;span class="s"&gt;--secret-string "$KEY"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload logs on failure&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;failure()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8&lt;/span&gt;  &lt;span class="c1"&gt;# v4.3.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;acme-logs-${{ matrix.domain }}&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~/.acme.sh/acme.log&lt;/span&gt;
          &lt;span class="na"&gt;retention-days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deliverable 2: Pre-Deployment Validation Script
&lt;/h2&gt;

&lt;p&gt;This bash script runs in CI before deploying certificate updates to production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="c"&gt;# validate-cert.sh - Run in CI to enforce certificate quality gates&lt;/span&gt;

&lt;span class="nv"&gt;DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;CERT_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Gate 1: Certificate expiry must be &amp;gt; 30 days&lt;/span&gt;
&lt;span class="nv"&gt;EXPIRY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl x509 &lt;span class="nt"&gt;-enddate&lt;/span&gt; &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CERT_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-f2&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;EXPIRY_EPOCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EXPIRY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;NOW_EPOCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;DAYS_REMAINING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;EXPIRY_EPOCH &lt;span class="o"&gt;-&lt;/span&gt; NOW_EPOCH&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;86400&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$DAYS_REMAINING&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 30 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: Certificate expires in &lt;/span&gt;&lt;span class="nv"&gt;$DAYS_REMAINING&lt;/span&gt;&lt;span class="s2"&gt; days (minimum 30)"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Gate 2: Subject Alternative Name must match domain&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-text&lt;/span&gt; &lt;span class="nt"&gt;-in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CERT_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"DNS:&lt;/span&gt;&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: Certificate SAN does not include &lt;/span&gt;&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Gate 3: Certificate chain must be complete&lt;/span&gt;
&lt;span class="nv"&gt;CHAIN_DEPTH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl crl2pkcs7 &lt;span class="nt"&gt;-nocrl&lt;/span&gt; &lt;span class="nt"&gt;-certfile&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CERT_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  openssl pkcs7 &lt;span class="nt"&gt;-print_certs&lt;/span&gt; &lt;span class="nt"&gt;-noout&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"subject="&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$CHAIN_DEPTH&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 2 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: Certificate chain incomplete (depth: &lt;/span&gt;&lt;span class="nv"&gt;$CHAIN_DEPTH&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PASS: Certificate valid for &lt;/span&gt;&lt;span class="nv"&gt;$DAYS_REMAINING&lt;/span&gt;&lt;span class="s2"&gt; days with complete chain"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  War Story: The Unpinned Action Incident
&lt;/h2&gt;

&lt;p&gt;In March 2025, a supply chain compromise in a popular GitHub Action inserted credential exfiltration code. We discovered our runner logs were being uploaded to an external S3 bucket—logs that contained temporary AWS credentials.&lt;/p&gt;

&lt;p&gt;The root cause: unpinned action versions using &lt;code&gt;@main&lt;/code&gt; tags. After incident response, we implemented two policies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pin all actions to full commit SHAs&lt;/strong&gt; (not tags, which are mutable)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weekly Dependabot PRs&lt;/strong&gt; to update pinned SHAs with security review&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Lessons from Production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Concurrency control matters.&lt;/strong&gt; Early versions allowed parallel renewal jobs, which triggered Let's Encrypt rate limits. The &lt;code&gt;cancel-in-progress: false&lt;/code&gt; directive prevents workflow cancellations mid-validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache keys must version-bump on breaking changes.&lt;/strong&gt; When we migrated from RSA to ECDSA certificates, stale cached account data caused validation failures. Explicit cache versioning (&lt;code&gt;-v2&lt;/code&gt; suffix) gives us a manual invalidation lever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Matrix builds isolate blast radius.&lt;/strong&gt; With &lt;code&gt;fail-fast: false&lt;/code&gt;, a failure renewing one domain doesn't block others. The tradeoff: longer wall-clock time, but acceptable for weekly runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Integrating acme.sh into GitHub Actions eliminated an entire class of operational incidents. The workflow runs unattended, with Slack notifications on failure and certificate metrics exported to our observability stack.&lt;/p&gt;

&lt;p&gt;The full validation script and workflow are production-tested across 40+ domains handling 2M+ requests daily. Certificate expiry hasn't triggered a page in 18 months.&lt;/p&gt;

</description>
      <category>github</category>
      <category>devops</category>
      <category>ci</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
