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:
- Despite Terraform being a state-based infrastructure management tool, we are using it as a one-off script here, so there is no reason to keep the state.
- Terraform state file contains the value of the secrets in plain-text, so you should not keep it around.
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.