Using Terraform To Sync Secrets Across Multiple Repositories For GitHub Free Account

After eons of SVN and CVS (anybody remember those?), I have been managing hundreds of Git repositories, mostly on GitHub. Out of those repositories, plenty are on Enterprise and Team accounts, but most are on Free accounts. The latter there is a bit of a pain to manage as it doesn’t have the ability to natively sync secrets across repositories.

This means that if I want to update a secret, I have to do it manually on each repository. Multiply this to hundreds of repositories, and it becomes way too tedious for any human to tolerate. So I ended up automating it using Terraform.

Secrets Storage

As the source of truth on this blog post, I store the secrets in AWS secure string Systems Manager parameters, though in practice you should consider using AWS Secrets Manager. Note that you can modify the Terraform code to retrieve the secrets from another source of your choosing.

Just as an example here, I have three artifact registry token secrets for publishing Python, Ruby, and node.js packages, and I also have a GitHub token. The SSM parameters are named as follows:

  • /secrets/pypi-token
  • /secrets/rubygems-token
  • /secrets/npmjs-token
  • /secrets/github-token

The intention here is to have PyPI token available as GitHub Actions secrets to all Python repositories, RubyGems token to all Ruby repositories, and npmjs token to all node.js repositories. The GitHub token is to be made available to all repositories.

Configuration Variables

The repository names are configured as Terraform variables. First, create a file called variables.tf where the variables are defined as follows:

variable "python_repos" {
  type        = list
  default     = []
  description = "Python repositories"
}

variable "ruby_repos" {
  type        = list
  default     = []
  description = "Ruby repositories"
}

variable "nodejs_repos" {
  type        = list
  default     = []
  description = "node.js repositories"
}

variable "github_repos" {
  type        = list
  default     = []
  description = "GitHub repositories"
}

And then create repos.tfvars file where you configure the repository names:

nodejs_repos = [
  "nodejs-repo-1",
  "nodejs-repo-2",
  "nodejs-repo-3"
]

python_repos = [
  "python-repo-1"
]

ruby_repos = [
  "ruby-repo-1",
  "ruby-repo-2"
]

github_repos = [
  "nodejs-repo-1",
  "nodejs-repo-2",
  "nodejs-repo-3",
  "python-repo-1",
  "ruby-repo-1",
  "ruby-repo-2"
]

AWS and GitHub Providers

The Terraform code requires AWS and GitHub providers to be configured. Create providers.tf file with the following content:

provider "aws" {
  region  = "ap-southeast-2"
}

# Note: requires GITHUB_TOKEN environment variable to be set
provider "github" {
}

Obviously you can specify the AWS provider’s region to match the region where you store your AWS Systems Manager parameters.

You also need to setup GITHUB_TOKEN environment variable to be consumed by the GitHub provider for authentication purpose. This token should have the right permissions to create and update GitHub Actions secrets on the configured repositories.

Terraform Code

Then create main.tf file with the content further below. The pattern here is that you retrieve the secrets using data.aws_ssm_parameter and then set them as GitHub Actions secrets using resource.github_actions_secret.

### PyPI token

data "aws_ssm_parameter" "pypi_token" {
  name = "/secrets/pypi-token"
}

resource "github_actions_secret" "pypi_token" {
  for_each        = toset(var.python_repos)
  repository      = each.key
  secret_name     = "PYPI_TOKEN"
  plaintext_value = data.aws_ssm_parameter.pypi_token.value
}

### RubyGems token

data "aws_ssm_parameter" "rubygems_token" {
  name = "/secrets/rubygems-token"
}

resource "github_actions_secret" "rubygems_token" {
  for_each        = toset(var.ruby_repos)
  repository      = each.key
  secret_name     = "RUBYGEMS_TOKEN"
  plaintext_value = data.aws_ssm_parameter.rubygems_token.value
}

### npmjs token

data "aws_ssm_parameter" "npmjs_token" {
  name = "/secrets/npmjs-token"
}

resource "github_actions_secret" "npmjs_token" {
  for_each        = toset(var.nodejs_repos)
  repository      = each.key
  secret_name     = "NPMJS_TOKEN"
  plaintext_value = data.aws_ssm_parameter.npmjs_token.value
}

### GitHub token

data "aws_ssm_parameter" "github_token" {
  name = "/secrets/github-token"
}

resource "github_actions_secret" "github_token" {
  for_each        = toset(var.github_repos)
  repository      = each.key
  secret_name     = "GH_TOKEN"
  plaintext_value = data.aws_ssm_parameter.github_token.value
}

Make It Happen

Just like any Terraform code, you can then run the usual plan/apply:

terraform apply -var-file=repos.tfvars

And part of the output would look like this:

After the Terraform run, you should be able to see the secrets named PYPI_TOKEN, RUBYGEMS_TOKEN, NPMJS_TOKEN, and GH_TOKEN in the GitHub repositories’ settings.

There’s one more thing that you should remember, you should delete the Terraform state file terraform.tfstate for two reasons:

Conclusion

This is a simple example of how you can use Terraform to sync secrets across repositories for GitHub Free account. You can modify the Terraform code to retrieve the secrets from other sources, and you can also modify the resource to other type of secrets other than GitHub Actions secret.

The main benefit from this approach is that you can easily update the secrets in one place, specially when you rotate the secrets (those tokens in my example), and then sync them across repositories. This is especially useful if you have hundreds of repositories.

The purist would argue that it might not be the best to use Terraform to perform a stateless action. I agree with that, but I also think that this is a pragmatic approach and it simply works. Consider this the poor man’s way of syncing secrets across multiple repositories for GitHub Free accounts.

The code snippets above are also available from an example repo at https://github.com/cliffano/tf-github-secrets-sync-example.

Share Comments
comments powered by Disqus