Using AWS Secrets Manager in Concourse

Continuous integrationTerraformAWS Secrets ManagerConcourse CI
Keeping it secret
by Hays

When I wrote about modernizing build pipelines I had a ton of content left that did not quite fit the article. I am going to dump some of that here as small articles. I believe they could be useful as a starting point to go towards a better CI setup.

The first thing I want to talk about is connecting Concourse CI with AWS Secrets Manager. Why would you want to do that, I picture you asking?

Well, if you have pipelines building stuff, they will probably want to access some secrets at some point. It could be a deploy key to access Github, or credentials for npm or something else. You don’t want to store this in plaintext. If you have had to maintain credentials manually for something like Jenkins you know that can really suck. But it does not have to. An alternative is to connect Concourse and the AWS Secrets Manager, using Terraform to keep everything as code.

Concourse supports a bunch of credentials managers out of the box. Once you have it configured, your jobs and tasks can access secrets in a very convenient way. It looks as simple as this

platform: linux
  - name: git
  - name: shared-tasks
  NPM_TOKEN: ((npm_auth_token))
  path: sh
  dir: git
  - -ec
  - |
    ./go linter-js

with that, the linter task will have access to the NPM_TOKEN, which is needed to pull from a private npm repository, in a safe manner. But with a lot of convenience for whoever is writing this pipeline and doesn’t want to do a lot of plumbing.

I gotta get me some of this

First, concourse needs to be started with the right settings, so that it will know to use ASM with the right region, and the right path

/usr/local/bin/concourse web \
  --aws-secretsmanager-region=us-east-1 \
  --aws-secretsmanager-team-secret-template=/concourse-ci/{{.Secret}} \

You can use a lot more granularity, and have a {{.Team}} or {{.Pipeline}} in there, but for this case we want just simple secrets. With this, the instance will be able to find the secrets under the given path with the syntax mentioned above.

Give me the rights

We are not yet there, though. Concourse needs to be able to access those secrets. You can pass it credentials, but a more secure method is to define an IAM Role, as the documentation mentions. This example will work if you are running your CI on EC2. First, the policy

resource "aws_iam_policy" "concourse-ci-web" {
  name   = "${}"
  policy = "${data.aws_iam_policy_document.concourse-ci.json}"

data "aws_iam_policy_document" "concourse-ci" {
  statement {
    effect = "Allow"

    actions = [

    resources = [
      "${formatlist("arn:aws:kms:*:*:key/%s", var.asm_key_ids)}",

then that policy gets connected to a role that is assumed by EC2

resource "aws_iam_role" "concourse-ci-web" {
  name               = "concourse-ci-web"
  assume_role_policy = "${data.aws_iam_policy_document.assume_role_policy_ec2.json}"

resource "aws_iam_instance_profile" "concourse-ci-web" {
  name = "${}"
  role = "${}"

resource "aws_iam_role_policy_attachment" "concourse-ci-web" {
  policy_arn = "${aws_iam_policy.concourse-ci-web.arn}"
  role       = "${}"

and finally the policy for assuming the role

data "aws_iam_policy_document" "assume_role_policy_ec2" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = [""]

That’s it?

Well, mostly. The example is not complete, and there will be some more code needed to make it work, but this is the gist of it. The best part is that you set it up once, and then you can access the secrets that you define in that namespace, just like that.