Full notes: KMS & GitHub App Private Key Security β†’

Key Concepts

What is KMS?

KMS (Key Management Service) is a managed service (GCP, AWS, Azure) that centralizes cryptographic key storage and operations. The fundamental principle: your application never sees raw key material β€” all crypto operations (encrypt, decrypt, sign, verify) happen inside the KMS boundary, often backed by HSMs. Core capabilities include key generation/import (BYOK), encrypt/decrypt, asymmetric signing, automatic key rotation, and fine-grained IAM with full audit logging of every cryptographic operation.

Why Use KMS for a GitHub App?

Without KMS, the private key sits as a file or env var in your runtime β€” if the app is compromised, the key is stolen. With KMS, the key only exists inside KMS. Your app says β€œsign this for me” and gets back a signature. Even a full compromise only allows signing requests (rate-limited, audit-logged) β€” the key itself cannot be exfiltrated. Additional benefits: hardware isolation, Cloud Audit Logs for every operation, and central IAM-based access control.

Step-by-Step Setup

Step 1 β€” Convert key format: GitHub gives a PEM file; KMS import requires PKCS#8 DER. Convert with openssl pkcs8 -topk8 -inform PEM -outform DER -in key.pem -out key.pkcs8 -nocrypt. Delete the converted file after import.

Step 2a β€” Create Key Ring (Terraform): google_kms_key_ring resource with a name and location (e.g., global).

Step 2b β€” Create Import Job: gcloud kms import-jobs create with rsa-oaep-4096-sha256-aes-256 import method and software protection level. This generates an ephemeral public key for one-time secure transfer of key material into KMS.

Step 3 β€” Create CryptoKey (Terraform): google_kms_crypto_key with purpose = "ASYMMETRIC_SIGN", import_only = true, skip_initial_version_creation = true, and algorithm RSA_SIGN_PKCS1_2048_SHA256. Key material comes from you, not generated by KMS.

Step 4 β€” Import the private key: gcloud kms keys versions import targeting the key ring, key, and import job. After this, you have a Primary CryptoKeyVersion ready for signing. Delete the local key.pkcs8.

Step 5 β€” Grant IAM (Terraform): google_kms_crypto_key_iam_member granting roles/cloudkms.signer to the designated service account. This is least-privilege β€” signing only, no decrypt or admin access.

Go Implementation

The app creates a KMS-backed signer using the Cloud KMS client and gcpkms package. The ghinstallation library builds JWTs with standard GitHub App claims (iss, iat, exp) and calls the signer to sign via KMS β€” the private key never enters app memory. The signed JWT is exchanged for an installation access token (1-hour validity). ghinstallation + go-github auto-refresh the token on expiry with no manual renewal logic.

Runtime Flow

App starts
  |
  v
Create KMS-backed signer (no key in memory)
  |
  v
ghinstallation builds JWT claims --> KMS signs it
  |
  v
Signed JWT --> GitHub API --> installation access token (1hr)
  |
  v
go-github client uses token for all API calls
(auto-refreshes on expiry)

Quick Reference

Without KMSWith KMS
Private key in env var or fileKey only inside KMS
Full key theft if app compromisedAttacker can only make signing requests (rate-limited, audit-logged)
No audit trail of key usageCloud Audit Logs for every sign operation

Key Terraform resources:

ResourceKey Settings
google_kms_key_ringname, location
google_kms_crypto_keypurpose: ASYMMETRIC_SIGN, import_only: true
google_kms_crypto_key_iam_memberrole: roles/cloudkms.signer

Go libraries: cloud.google.com/go/kms/apiv1, ghinstallation/v2 (with WithSigner), go-github, octo-sts/app/pkg/gcpkms

Key Takeaways

  • KMS ensures the private key never enters your application runtime β€” even a full compromise can’t exfiltrate it
  • roles/cloudkms.signer is the least-privilege IAM role for signing-only access
  • Key format conversion (PEM to PKCS#8 DER) is required before import β€” delete the local file afterward
  • The ghinstallation Go library natively supports custom signers, making KMS integration straightforward
  • Installation tokens auto-refresh (1-hour validity) with no manual renewal logic needed
  • Import jobs use RSA-OAEP-4096 with AES-256 wrapping for secure one-time key transfer into KMS