DEV Community

Roman Tsisyk
Roman Tsisyk

Posted on

Managing GitHub Actions Secrets via Git + Terraform

GitHub Actions Secrets are encrypted environment variables that you create in an organization or repository on GitHub. Secrets can be used to store things like API keys, access tokens, passwords, and other sensitive information. Secrets can be managed through the GitHub UI or API.

Unfortunately, GitHub doesn't provide any way to manage encrypted secrets via git. Yes, this sounds really strange that you need something other than git on GitHub, but it is. This article closes this gap by leveraging Terraform to populate GitHub Actions Secrets directly from the GitHub repository. GitHub uses a libsodium sealed box to encrypt secrets, and there is absolutely no risk in storing encrypted secrets in the repo itself.

Prerequisites

  1. GitHub repository
  2. Terraform (version 1.3.7 was tested)
  3. GitHub CLI a.k.a. gh (version 2.23.0 was tested)

Create Terraform stack

in-vars.tf:

variable "github_owner" {
  description = "GitHub user name"
  type        = string
}

variable "github_repository" {
  description = "GitHub repository name"
  type        = string
}
Enter fullscreen mode Exit fullscreen mode
  • github_owner will be your GitHub account name, like torvalds.
  • github_repository will be your GitHub repository name, like linux.

This example uses Terraform's GitHub integration to manage GitHub resources via Terraform.

providers.tf:

terraform {
  required_version = "~> 1.0"
  required_providers {
    github = {
      source  = "integrations/github"
      version = "~> 5.18"
    }
  }
}

provider "github" {
  owner = var.github_owner
}
Enter fullscreen mode Exit fullscreen mode

The next file is the primary Terraform configuration file that instructs Terraform to update GitHub Actions Secrets from secrets.yaml file:

data "github_user" "current" {
  username = ""
}

data "github_repository" "example" {
  full_name = "${var.github_owner}/${var.github_repository}"
}

resource "github_actions_environment_secret" "example" {
  for_each = merge([for environment, secrets in yamldecode(file("secrets.yaml"))["secrets"] : {
    for name, value in secrets : "${environment}:${name}" =>
    {
      environment = environment
      name        = name
      value       = value
    }
  }]...)
  repository      = data.github_repository.example.name
  environment     = each.value.environment
  secret_name     = each.value.name
  encrypted_value = each.value.value
}
Enter fullscreen mode Exit fullscreen mode

All secrets will be stored encrypted in secrets.yaml file:

secrets.yaml:

#
# This file contains encrypted secrets for GitHub Actions
#
# All values in this file are encrypted by using public-key cryptography.
# GitHub uses different public key per each environment per each repository.
# To add a new secret, please run `gh secret set --no-store -e dev|stage|prod` in the root of this repo.
# Please make ensure that you are in the right repository and have the right value of `-e` flag.
#
# To apply values to GitHub, please run `terraform apply`.
#
secrets:
  # dev:
  #  HELLO: <encrypted value>
  #  WORLD: <encrypted value>
  dev: []
  stage: []
  prod: []
Enter fullscreen mode Exit fullscreen mode

Keep an empty list ([]) for each environment for now.

Generate Personal Access Token (PAT)

Go to https://github.com/settings/tokens and generate a new Personal Access Token (PAT) to access GitHub via API. At the moment of writing (2023-03-21), new fine-grained scoped tokens didn't work properly for managing Secrets.

Please grant repo (Full control of private repositories) permission to your token:

GitHub Personal Access Token

Create Environments

Environments are needed to protect your secrets. In this tutorial we will create dev, stage and prod environments. Only GitHub Workflows with dev, stage and prod tags will be able to access Secrets in corresponding environments. GitHub Environments can be restricted by branch names.

Go to your repository → Settings → Environments and add dev, stage, prod environments:

GitHub Environments

Generate Secrets

Encrypt secrets for each environment:

gh secret set --no-store -e dev
Enter fullscreen mode Exit fullscreen mode

Type secret value into the command line prompt. This command together with the --no-store option encrypts all data locally by using a public key from the specified GitHub Environment (i.e. dev, stage, prod). Sensitive data is not transmitted outside your machine. Save generated value into secrets.yaml:

secrets:
  dev:
    HELLO: QQYIZqerFfiarG+9VyGeDDqz/cMNL7OABaNjWMPOOD7qDO7IHUr+1RND8p5oilV8VaIgbso=
    WORLD: oxSzP+MNstOknjv/1Q3/m9q0fQAbaxny91DNd0tf2TV+pOBpgfqURA/0UrG1+V8OR3RHD7Y=
  stage:
    HELLO: d4gfSmhZolZSh/zWq3gh/hTMUiBm5bZyeJuHmKIkW3jl3pHX3E565vTpDBjHBVzydr8bMFw=
    WORLD: vVwxkPZY4Z6HYZKmfmy5PrPe7GPCqXav+59yPB7WzHe6dZ3B3VLxvei4PnO+hqIL4OoDlsQ=
  prod:
    HELLO: +DsS2fbRzLIDmUqqUC0+dZlq9tgYO2jufUGj3vGxpk/1pFgjLaPk03a6uLOGgha43nfNLxU=
    WORLD: 1Pxp9C8BK4L9Q1COBQe+3MjaJR7iyajM3iF5gkmBvxEg/9d38VBvJbLf3Zt+40qexk87ZnY=
Enter fullscreen mode Exit fullscreen mode

Apply Secrets

Now it is time to apply all the secrets to GitHub by using Terraform. Export environment variables to specify your GitHub name, repository and Personal Access Token (PAT).

export TF_VAR_github_owner=rtsisyk
export TF_VAR_github_repository=github-actions-secrets-terraform
export GITHUB_TOKEN=<Personal Access Token generated previously>
Enter fullscreen mode Exit fullscreen mode

Now run Terraform:

terraform apply
Enter fullscreen mode Exit fullscreen mode
[REDACTED]
Terraform will perform the following actions:

  # github_actions_environment_secret.example["dev:HELLO"] will be created
  + resource "github_actions_environment_secret" "example" {
      + created_at      = (known after apply)
      + encrypted_value = (sensitive value)
      + environment     = "dev"
      + id              = (known after apply)
      + repository      = "github-actions-secrets-terraform"
      + secret_name     = "HELLO"
      + updated_at      = (known after apply)
    }
[REDACTED]
Enter fullscreen mode Exit fullscreen mode

Terraform will apply all secrets to GitHub:

GitHub Environments with Secrets

GitHub Action Secrets

That is it. Now you can manage all GitHub Secrets in GitHub repo.

Notes Regarding Terraform State

Terraform is stateful and keeps information about all created resources in terraform.tfstate by default:

    {
      "mode": "managed",
      "type": "github_actions_environment_secret",
      "name": "example",
      "provider": "provider[\"registry.terraform.io/integrations/github\"]",
      "instances": [
        {
          "index_key": "dev:HELLO",
          "schema_version": 0,
          "attributes": {
            "created_at": "2023-03-20 09:29:43 +0000 UTC",
            "encrypted_value": "QQYIZqerFfiarG+9VyGeDDqz/cMNL7OABaNjWMPOOD7qDO7IHUr+1RND8p5oilV8VaIgbso=",
            "environment": "dev",
            "id": "github-actions-secrets-terraform:dev:HELLO",
            "plaintext_value": "",
            "repository": "github-actions-secrets-terraform",
            "secret_name": "HELLO",
            "updated_at": "2023-03-20 09:29:43 +0000 UTC"
          },
          "sensitive_attributes": [],
          "private": "bnVsbA==",
          "dependencies": [
            "data.github_repository.example"
          ]
        },
Enter fullscreen mode Exit fullscreen mode

All Secrets generated in this tutorial are encrypted by using libsodium sealed box. There is absolutely no risk in storing these encrypted strings in a git repository and having them in the Terraform state.

For github_actions_environment_secret resource in particular, you can actually drop Terraform state and Terraform will correctly update GitHub Actions Secrets on every run without any issues.

Links

Top comments (2)

Collapse
 
rsmets profile image
Ray Smets • Edited

An improvement would be to create the envs in tf too. You can do that with something like this:

locals {
  secrets = yamldecode(file("secrets.gh_repo_pub_key.enc.yaml"))["secrets"]

  # Flatten the variables map for use in for_each
  flatten_secrets = merge([
    for environment, secrets in local.secrets : {
      for name, value in secrets : "${environment}:${name}" => {
        environment = environment
        name        = name
        value       = value
      }
    }
  ]...)
}

data "github_repository" "repo" {
  full_name = var.repository_full_name
}

# ref: https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository_environment
resource "github_repository_environment" "env" {
  for_each = { for env, _ in local.secrets : env => env }

  environment = each.key
  repository  = data.github_repository.repo.name
}

# ref: https://registry.terraform.io/providers/integrations/github/latest/docs/resources/actions_secret
# ref: https://dev.to/rtsisyk/managing-github-actions-secrets-via-git-terraform-2f81
resource "github_actions_environment_secret" "secrets" {
  for_each = local.flatten_secrets

  repository      = data.github_repository.repo.name
  environment     = each.value.environment
  secret_name     = each.value.name
  encrypted_value = each.value.value
}

  depends_on = [
    github_repository_environment.env
  ]
Enter fullscreen mode Exit fullscreen mode
Collapse
 
rsmets profile image
Ray Smets

Sweet, thanks for the post. It helped me sort out libsodium for the encrypted action secrets. I normally use sops for secret encryption. Interesting GH went with libsodium.