Introduction
With the recent success of Github actions you can automate lots of things whenever something in your repos changes, e.g. automatically generate static HTML content (using hugo) and push it to some repository for which GitHub Pages has been configured. Check this awesome actions list for more use cases.
Using encrypted secrets defined either per repository or organization, you can bring your Github workflow to the next level: Authenticate against APIs, login to different services while keeping your secrets/credentials away from your repositories. As a general you should never store credentials in your repositories, even if they’re private. Misconfigurations happen all the time and private repos can become public ones without further notice.
In this post I want to show the Golang way how to update Github secrets in some repository. These secrets (more concrete an AWS IAM access key ID and an AWS IAM access secret key) should be used to interact with AWS. Rotating these keys regularly is essential and also part of the AWS access keys best practices.
Make sure you also check github.com/dorneanu/access-key-rotator for the complete project.
Clean architecture
I’m obsessed with clean code, clean architecture and almost everything that has an easy to understand structure.
First of all I’ll start with the use cases which describe what the application is capabable of doing. In our case we have
- Rotate keys
- Given a
key managerthe existing access keys will be rotated
- Given a
- Upload secrets
- using a
secrets storewe’ll upload the encrypted access key to some storage for later usage
- using a
The KeyManager and the SecretsStore are interfaces to be implemented by different service providers. What the both have in common is the
AccessKey data structure which holds everything we need to know about an access key.

Figure 1: Interfaces using entities
Now that we have defined the general application design, let’s go more into details and see which components have to implement the declared interfaces:
KeyManager- a key manager is something that holds/stores your access keys and provides functionalities (CRUD: create, read, update, delete) in order to manage those
- examples: AWS IAM, Google Cloud IAM, Azure IAM
SecretsStore- something that stores your access keys in a secure manner
- examples: GitHub Secrets, Gitlab Secrets, LastPass
ConfigStore- something related to a parameter store
- examples: AWS SecretsManager, Google Cloud Secret Manager
Each of these components (using 3rd-party libraries etc.) will need to implement the correspondig interface.

Figure 2: Components implementing interfaces
AWS Golang SDK v2
I’ll be using the latest Golang SDK which is v2. In order to manage the IAM access keys we’re going to need these endpoints:
- list all available access keys using ListAccessKeysV2
- generate new IAM access key using CreateAccessKeyv2
- delete old access keys using DeleteAccessKeyv2
In order the make the code more testable I’ll be using an interface called IAMAPI which should contain all methods an IAM API real implementation
should provide. Generating mocks should be then also an easy task as described in Unit Testing with the AWS SDK for Go V2.
|
|
Additionally I’ll use an own Configuration type meant to hold all information my applications needs. I find github.com/kelseyhightower/envconfig to be quite
handy when you have to deal with environment variables:
|
|
List/Fetch all available IAM keys
First of all let’s list all available IAM access keys.
|
|
|
|
As you can see there is an IAM access key with the ID AKIAWSIW5AN47M5YY72J and it’s active.
Generate new IAM access key
In the next step we’ll generate a new pair of access key. Therefore we’ll extend the IAMAPI interface with a 2nd method:
|
|
Creating a new key pair should also be straght forwards:
|
|
And then in the main() we add:
|
|
And if we run it, we’ll get the new key id and the secret key:
|
|
Delete old access key
We’ll extend the IAMAPI interface again:
|
|
The DeleteAccessKey will also need an access key ID and an username:
|
|
For this example we’ll just delete the previously created IAM access key:
|
|
Github setup
The Github implementation will have to satisfy the SecretsStore interface:
|
|
SecretsStore implementation
As we have done with AWS we’ll try to decouple everything and have less cohesion. This will make every part of our code testable.
The GithubSecretsStore (implementing SecretsStore) will look like this:
|
|
Make secrets service abstract
The secretsClient is a service that allows us to create, upload and delete secrets using Github’s Secrets API. The GithubSecretsService
will have following definition (make sure to have a look at the methods provided by the ActionsService):
|
|
This way we can create a GithubSecretsStore with a mocked version of GithubSecretsService. But there is still something missing. Of course, the Github client itself:
|
|
Use a real Github client
And how does this structure fit together with the service and the store? Following constructor should provide the answer:
|
|
Here I initialize a new github.Client by using an OAUTH2 token. Afterwards I return client.Actions which btw satisfies the GithubSecretsService interface. Now let’s code a constructor for the GithubSecretsStore:
|
|
Here NewGithubSecretsStore expects a GithubSecretsService and some other additional information (repository owner/name). As the Liskow Substitution Principle says:
Express dependencies between packages in terms of interfaces and not concrete types
in NewGithubSecretsStore we don’t expect an ActionsService as it is returned by github.Client.Actions. So, in order to glue everything together we’ll have to
- first create a concrete implementation of
GithubSecretsService - and then create a new
GithubSecretsStorewith that concrete implementation
So in the real code this will look like this:
|
|
Conclusion
Setting up a project with clean code in mind is not an easy task. You have to abstract things and always keep in mind:
How can you know your code works? That’s easy. Test it. Test it again. Test it up. Test it down. Test it seven ways to Sunday – Source
And how do you make sure your code is testable? By using abstractions instead of concrete implementations and making each single part of your code mockable aka testable.