JSON Wrangling with Go

Golang is a fantastic language but at first glance it is a bit clumsy when it comes to JSON in contrast to other languages such as Python or Javascript. Having said that once you master the concepts involved with JSON wrangling using Go it is equally as functional – with added type safety and performance.

In this article we will build a program in Golang to parse a JSON file containing a collection held in a named key – without knowing the structure of this object, we will expose the schema for the object including data types and recurse the object for its values.

This example uses a great Go package called tablewriter to render the output of these operations using a table style result set.

The program has describe and select verbs as operation types; describe shows the column names in the collection and their respective data types, select prints the keys and values as a tabular result set with column headers for the keys and rows containing their corresponding values.

Starting with this:

We will end up with this when performing a describe operation:

And this when performing a select operation:

Now let’s talk about how we got there…

The JSON package

Support for JSON in Go is provided using the encoding/json package, this needs to be imported in your program of course… You will also need to import the reflect package – more on this later. io/ioutil is required to read the data from a file input, there are other packages included in the program that are removed for brevity:

Reading the data…

We will read the data from the JSON file into a variable called body, note that we are not attempting to deserialize the data at this point. This is also a good opportunity to handle any runtime or IO errors that occur here as well.

The interface…

We will declare an empty interface called data which will be used to decode the json object (of which the structure is not known), we will also create an abstract interface called colldata to hold the contents of the collection contained inside the JSON object that we are specifically looking for:

Validating…

Next we need to validate that the input is a valid JSON object, we can use the json.Valid(body) method to do this:

Unmarshalling…

Now the interesting bits, we will deserialize the JSON object to the empty data interface we created earlier using the json.Unmarshal() method:

Note that this operation is another opportunity to catch unexpected errors and handle them accordingly.

Checking the type of the object using reflection…

Now that we have serialized the JSON object into the data interface, there are several ways we can inspect the type of the object (which could be a map or an array). One such way is to use reflection. Reflection is the ability of a program to inspect itself at runtime. An example is shown here:

This instruction would produce the following output for our zones.json file:

The type switch…

Another method to decode the type of the data object (and any objects nested as elements or keys within the data object), is to use the type switch, an example of a type switch function is shown here:

Finding the nested collection and recursing it…

The aim of the program is to find a collection (an array of maps) nested in a JSON object. The maps with each element of the array are unknown at runtime and are discovered through recursion.

If we are performing a describe operation, we only need to parse the first element of the collection to get the key names and the data type of the values (for which we will use the same getObjectType function to perform a type switch.

If we are performing a select operation, we need to parse the first element to get the column names (the keys in the map) and then we need to recurse each element to get the values for each key.

If the element contains a key named id or name, we will place this at the beginning of the resultant record, as maps are unordered by definition.

The output…

As mentioned, we are using the tablewriter package to render the output of the collection as a pretty printed table in our terminal. As wrap around can get pretty ugly an additional maxfieldlen argument is provided to truncate the values if needed.

In summary…

Although it is a bit more involved than some other languages, once you get your head around processing JSON in Go, the possibilities are endless!

Full source code can be found at: https://github.com/gamma-data/json-wrangling-with-golang

Forseti Terraform Validator: Enforcing resource policy compliance in your CI pipeline

Terraform is a powerful tool for managing your Infrastructure as Code. Declare your resources once, define their variables per environment and sleep easy knowing your CI pipeline will take care of the rest.

But… one night you wake up in a sweat. The details are fuzzy but you were browsing your favourite cloud provider’s console – probably GCP 😉 – and thought you saw a bucket had been created outside of your allowed locations! Maybe it even had risky access controls.

You go brush it off and try to fall back to sleep, but you can’t quite push the thought from your mind that somewhere in all that Terraform code, someone could be declaring resources in unapproved locations, and your CICD pipeline would do nothing to stop it. Oh the regulatory implications.

Enter Terraform Validator by Forseti

Terraform Validator by Forseti allows you to declare your Policy as Code, check compliance of your Terraform plans against said Policy, and automatically fail violating plans in a CI step. All without setting up servers or agents.

You’re going to learn how to enforce policy on GCP resources like BigQuery, IAM, networks, MySQL, Google Kubernetes Engine (GKE) and more. If you’re particularly crafty, you may be able to go beyond GCP.

Forseti’s suite of solutions are GCP focused and allow a wide range of live config validation, monitoring and more using the Policy Library we’re going to set up. These additional capabilities require additional infrastructure. But we’re going one step at a time, starting with enforcing policy during deployment.

Getting Started

Let’s assume you already have an established CICD pipeline that uses Terraform, or that you are content to validate your Terraform plans locally for now. In that case, we need just two things:

  1. A Policy Library
  2. Terraform Validator

It’s that simple! No new servers, agents, firewall rules, extra service accounts or other nonsense. Just add Policy Library, the Validator tool and you can enforce policy on your Terraform deployments.

We’re going to tinker with some existing GCP-focused sample policies (aka Constraints) that Forseti makes available. These samples cover a wide range of resources and use cases, so it is easy to adjust what’s provided to define your own Constraints.

Policy Library

First let’s open up some of Forseti’s pre-defined constraints. We’ll copy them into our own Git repository and adjust to create policies that match our needs. Repeatable and configurable – that’s Policy as Code at work.

Concepts

In the world of Forseti and in particular Terraform Validator, Policies are defined and understood via easy to read YAML files known as Constraints

There is just enough information in a Constraint file for to make clear its purpose and effect, and by tinkering lightly with a pre-written Constraint you can achieve a lot without looking too deeply into the inner workings . But there’s more happening than meets the eye.

Constraints are built on Templates – which are like forms with some extra bits waiting to be completed to make a Constraint. Except there’s a lot more hidden away that’s pretty cool if you want to understand it.

Think of a Template as a ‘Class’ in the OOP sense, and of a Constraint as an instantiated Template with all the key attributes defined.

E.g. A generic Template for policy on bucket locations and a Constraint to specify which locations are relevant in a given instance. Again, buckets and locations are just the basic example – the potential applications are far greater.

Now the real magic is that just like a ‘Class’, a Template contains logic that makes everything abstracted away in the Constraint possible. Templates contain inline Rego (ray-go), borrowed lovingly by Forseti from the Open Policy Agent (OPA) team.

Learn more about Rego and OPA here to understand the relationship to our Terraform Validator.

But let’s begin.

Set up your Policies

Create your Policy Library repository

Create your Policy Library repository by cloning https://github.com/forseti-security/policy-library into your own VCS.

This repo contains templates and sample constraints which will form the basis of your policies. So get it into your Git environment and clone it to local for the next step.

Customise sample constraints to fit your needs

As discussed in Concepts, Constraints are defined Templates, which make use of Rego policy language. Nice. So let’s take a sample Constraint, put it in our Policy Library and set the values to what we need. It’s that easy – no need to write new templates or learn Rego if your use case is covered.

In a new branch…

  1. Copy the sample Constraint storage_location.yaml to your Constraints folder.
    $ cp policy-library/samples/storage_location.yaml policy-library/policies/constraints/storage_location.yaml
  2. Replace the sample location (asia-southeast1) in storage_location.yaml with australia-southeast1.
    spec:
    severity: high
    match:
    target: ["organization/*"]
    parameters:
    mode: "allowlist"
    locations:
    - australia-southeast1
    exemptions: []
  3. Push back to your repo – not Forseti’s!
    $ git push https://github.com/<your-repository>/policy-library.git

Policy review

There you go – you’ve customised a sample Constraint. Now you have your own instance of version controlled Policy-as-Code and are ready to apply the power of OPA’s Rego policy language that lies within the parent Template. Impressively easy right?

That’s a pretty simple example. You can browse the rest of Forseti’s Policy Library to view other sample Constraints, Templates and the Rego logic that makes all of this work. These can be adjusted to cover all kinds of use cases across GCP resources.

I suggest working with and editing the sample Constraints before making any changes to Templates.

If you were to write Rego and Templates from scratch, you might even be able to enforce Policy as Code against non-GCP Terraform code.

Terraform Validator

Now, let’s set up the Terraform Validator tool and have it compare a sample piece of Terraform code against the Constraint we configured above. Keep in mind you’ll want to translate what’s done here into steps in your CICD pipeline.

Once the tool is in place, we really just run terraform plan and feed the output into Terraform Validator. The Validator compares it to our Constraints, runs all the abstracted logic we don’t need to worry about and returns 0 or 2 when done for pass / fail respectively. Easy.

So using Terraform if I try to make a bucket in australia-southeast1 it should pass, if I try to make one in the US it should fail. Let’s set up the tool, write some basic Terraform and see how we go.

Setup Terraform Validator

Check for the latest version of terraform-validator from the official terraform-validator GCS bucket.

Very important when using tf version 0.12 or greater. This is the easy way – you can pull from the Terraform Validator Github and make it yourself too.

$ gsutil ls -r gs://terraform-validator/releases

Copy the latest version to the working dir

$ gsutil cp gs://terraform-validator/releases/2020-03-05/terraform-validator-linux-amd64 .

Make it executable

$ chmod 755 terraform-validator-linux-amd64

Ready to go!

Review your Terraform code

We’re going to make a ridiculously simple piece of Terraform that tries to create one bucket in our project to keep things simple.

main.tf

resource "google_storage_bucket" "tf-validator-demo-bucket" {  
  name          = "tf-validator-demo-bucket"
  location      = "US"
  force_destroy = true

  lifecycle_rule {
    condition {
      age = "3"
    }
    action {
      type = "Delete"
    }
  }
}

This is a pretty standard bit of Terraform for a GCS bucket, but made very simple with all the values defined directly in main.tf. Note the location of the bucket – it violates our Constraint that was set to the australia-southeast1 region.

Make the Terraform plan

Warm up Terraform.
Double check your Terraform code if there are any hiccups.

$ terraform init

Make the Terraform plan and store output to file.

$ terraform plan --out=terraform.tfplan

Convert the plan to JSON

$ terraform show -json ./terraform.tfplan > ./terraform.tfplan.json

Validate the non-compliant Terraform plan against your Constraints, for example

$ ./terraform-validator-linux-amd64 validate ./tfplan.tfplan.json --policy-path=../repos/policy-library/

TA-DA!

Found Violations:

Constraint allow_some_storage_location on resource //storage.googleapis.com/tf-validator-demo-bucket: //storage.googleapis.com/tf-validator-demo-bucket is in a disallowed location.

Validate the compliant Terraform plan against your Constraints

Let’s see what happens if we repeat the above, changing the location of our GCS bucket to australia-southeast1.

$ ./terraform-validator-linux-amd64 validate ./tfplan.tfplan.json --policy-path=../repos/policy-library/

Results in..

No violations found.

Success!!!

Now all that’s left to do for your Policy as Code CICD pipeline is to configure the rest of your Constraints and run this check before you go ahead and terraform apply. Be sure to make the apply step dependent on the outcome of the Validator.

Wrap Up

We’ve looked at how to apply Policy as Code to validate our Infrastructure as Code. Sounds pretty modern and DevOpsy doesn’t it.

To recap, we learned about Constraints, which are fully defined instances of Policy as Code. They’re based on YAML Templates that refer to the OPA policy language Rego, but we didn’t have to learn it 🙂

We created our own version controlled Policy Library.

Using the above learning and some handy pre-existing samples, we wrote policies (Constraints) for GCP infrastructure, specifying a whitelist for locations in which GCS buckets could be deployed.

As mentioned there are dozens upon dozens of samples across BigQuery, IAM, networks, MySQL, Google Kubernetes Engine (GKE) and more to work with.

Of course, we stored these configured Constraints in our version-controlled Policy Library.

  • We looked at a simple set of Terraform code to define a GCS bucket, and stored the Terraform plan to a file before applying it.
  • We ran Forseti’s Terraform Validator against the Terraform plan file, and had the Validator compare the plan to our Policy Library.
  • We saw that the results matched our expectations! Compliance with the location specified in our Constraint passed the Validator’s checks, and non-compliance triggered a violation.

Awesome. And the best part is that all this required no special permissions, no infrastructure for servers or agents and no networking.

All of that comes with the full Forseti suite of Inventory taking Config Validation of already deployed resources. We might get to that next time.

References:

https://github.com/GoogleCloudPlatform/terraform-validator https://github.com/forseti-security/policy-library https://www.openpolicyagent.org/docs/latest/policy-language/ https://cloud.google.com/blog/products/identity-security/using-forseti-config-validator-with-terraform-validator https://forsetisecurity.org/docs/latest/concepts/

Ansible Tower for Continuous Infrastructure

As infrastructure and teams scale, effective and robust configuration management requires growing beyond manual processes and local conventions. Fortunately, Ansible Tower (or the upstream Open Source project Ansible AWX) provides a perfect platform for configuration management at scale.

The Ansible Tower/AWX documentation and tutorials provide comprehensive information about the individual components.  However, assembling all the moving pieces into a whole working solution can involve some trial and error and reverse engineering in order to understand how the components relate to one another.  Ansible Tower, like the core Ansible solution, offers flexibility in how features assembled to support different typed of workflows. The types of workflows can include once-off initial configurations, ad-hoc system maintenance, or continuous convergence.

Continuous convergence, also referred to as desired state, regularly re-applies the defined configuration to infrastructure. This tends to ‘correct the drift’ often encountered when only applying the configuration on infrastructure setup. For example, a continuous convergence approach to configuration management could apply the desired configuration on a recurring schedule of every 30 minutes.  

Some continuous convergence workflow characteristics can include:

  • Idempotent Ansible roles. If there are no required configuration deviations, run will report 0 changes.
  • A source code repository per Ansible role, similar to the Ansible Galaxy approach,
  • A source code repository for Ansible playbooks that include the individual Ansible roles,
  • A host configured to provide one unique service function only,
  • An Ansible playbook defined for each unique service function that gets applied to the host,
  • Playbooks applied to each host on a repeating schedule.

One way to achieve a continuous convergence workflow combines the Ansible Tower components according to the following conceptual model.

The Workflow Components

Playbook and Role Source Code

Ansible roles contain the individual tasks, handlers, and content with a role responsible for the installation and configuration of a particular software service.

Ansible playbooks configure a host for a particular service function in the environment acting as a wrapper for the individual role based configurations.  All the roles expected to be applied to a host must be defined in the playbook.

Source Code Repositories

Role git repositories contain the versioned definition of a role, e.g. one git repository per individual role.  The roles are pulled into the playbooks using the git reference and tags, which pegs the role version used within a playbook.

Project git repositories group the individual playbooks into single collection, e.g. one git repository per set of playbooks.  As with roles, specific versions of project repositories are also identified by version tags. 

Ansible Tower Server

Two foundational concepts in Ansible Tower are projects and inventories. Projects provide access to playbooks and roles. Inventories provide the connection to “real” infrastructure.  Inventories and projects also provide authorisation scope for activities in Ansible Tower. For example, a given group can use the playbooks in Project X and apply jobs to hosts in Inventory Y.

Each Ansible Tower Project is backed by a project git repository.  Each repository contains the playbooks and included roles that can be applied by a given job.  The Project is the glue between the Ansible configuration tasks and the plays that apply the configuration.

Ansible Tower Inventories are sets of hosts grouped for administration, similar to inventory sets used when applying playbooks manually.  One option is to group hosts into Inventories by environment.  For example, the hosts for development may be in one Inventory while the hosts for production may be in another Inventory.  User authorisation controls are applied at the Inventory level.

Ansible Tower Inventory Groups define sub-sets of hosts within the larger Inventory.  These subsets can then be used to limit the scope of a playbook job.  One option is to group hosts within an Inventory by function.  For example, the hosts for web servers may be in one Inventory Group and the hosts for databases may be in another Inventory Group.  This enables one playbook to target one inventory group.  Inventory groups effectively provide metadata labels for hosts in the Inventory.

An Ansible Job Template determines the configuration to be applied to hosts.  The Job Template links a playbook from a project to an inventory.   The inventory scope can be optionally further limited by specifying inventory group limits.  A Job Template can be invoked either on an ad-hoc basis or via a recurring schedule.

Ansible Job Schedules define the time and frequency at which the configuration specified in the Job Template is applied.  Each Job Template can be associated with one or more Job Schedules.  A schedule supports either once-off execution, for example during a defined change window, or regularly recurring execution.  A job schedule that applies the desired state configuration with a frequency of 30 minutes provides an example of a job schedule used for a continuous convergence workflow.

“Real” Infrastructure

An Ansible Job Instance defines a single invocation of an Ansible Job Template, both for scheduled and ad-hoc invocations of the job template.  Outside of Ansible Tower, the Job Instance is the equivalent of executing the ansible-playbook command using an inventory file.

Host is the actual target infrastructure resources configured by the job instance, applying an ansible playbook of included roles.

A note on Ansible Variables

As with other features of Ansible and Ansible Tower, variables also offer flexibility in defining parameters and context when applying a configuration.  In addition to declaring and defining variables in roles and playbooks, variable definitions can also be defined in Ansible Tower job templates, inventory and inventory groups, and individual hosts.  Given the plethora of options for variable definition locations, without a set of conventions for managing variable values, debugging runtime issues with roles and playbooks can become difficult.  E.g. which value defined at which location was used when applying the role?

One example of variable definitions conventions could include:

  • Variables shall be given default values in the role, .e.g. in the ../defaults/main.yml file.
  • If the variable must have a ‘real’ value supplied when applying the playbook, the variable shall be defined with an obvious placeholder value which will fail if not overridden.
  • Variables shall be described in the role README.md documentation
  • Do not apply variables at the host inventory level as host inventory can be transient.
  • Variables that select specific capabilities within a role shall be defined at the Ansible Tower Inventory Group.  For example, a role contains the configuration definition for both master and work nodes.  The Inventory Group variables are used to indicate which hosts must have the master configuration and applied and which must have the worker configuration applied.
  • Variables that define the environment context for configuration shall be defined in the Ansible Tower Job Template.

Following these conventions, each of the possible variable definition options serves a particular purpose.  When an issue with variable definition does arise, the source is easily identified.

Managing Secrets in CICD Pipelines

Overview

With the adoption automation for deploying and managing application environments, protecting privileged accounts and credential secrets in a consistent, secure, and scalable manner becomes critical.  Secrets can include account usernames, account passwords and API tokens.  Good credentials management and secrets automation practices reduce the risk of secrets escaping into the wild and being used either intentionally (hacked) or unintentionally (accident).

  • Reduce the likelihood of passwords slipping into source code commits and getting pushed to code repositories, especially public repositories such as github.
  • Minimise the secrets exposure surface area by reducing the number of people who require knowledge of credentials.  With an automated credentials management process that number can reach zero.
  • Limit the useful life of a secret by employing short expiry times and small time-to-live (TTL) values.  Automation enables reliable low-effort secret re-issue and rotation.

Objectives

The following objectives have been considered in designing a secrets automation solution that can be integrated into an existing CICD environment.

  • Integrate into an existing CICD environment without requiring an “all or nothing” implementation.  Allow existing jobs to operate alongside jobs that have been converted to the new secrets automation solution.
  • A single design that can be applied across different toolchains and deployment models.  For example, deployment to a Kubernetes environment can use the same secrets management process as an application installation on a virtual machine.  Similarly, the design can be used with different CICD tools, such as GitLab-CI, Travis-CI, or other build and deploy automation tool.
  • Multi-cloud capable by limiting coupling to a specific hosting environment or cloud services provider.
  • The use of secrets (or not) can be decided at any point in time, without requiring changes to the CICD job definition, similar to the use of feature flags in applications.
  • Enable changes to secrets, either due to rotation or revocation, to be maintained from a central service point.  Avoid storing the same secret multiple times in different locations.
  • Secrets organised in predictable locations in a “rest-ish” fashion by treating secrets and credentials as attributes of entities.
  • Use environment variables as the standard interface between deployment orchestration and deployed application, following the 12 Factor App approach.

Solution

  • Secrets stored centrally in Hashicorp Vault.
  • CICD jobs retrieve secrets from Vault and configure the application deployment environment.
  • Deployed applications use the secrets supplied by CICD job to access backend services.
CICD Secrets with Vault

Storing Secrets

Use Vault by Hashicorp as a centralised secrets storage service.  The CICD service retrieves secrets information for integration and deployment jobs.  Vault provides a flexible set of features to support numerous different workflows and available as either Vault Open Source or Vault Enterprise.  The secrets management pattern described uses the Vault Open Source version.  The workflow described here can be explored using Vault in the unsecured development mode, however, a properly configured and managed Vault service is required for production use.

Vault supports a number of secrets backends and access workflow models.  This solution makes use of the Vault AppRole method, which is designed to support machine-to-machine automated workflows.  With the AppRole workflow model human access to secrets is minimised through the use of access controls and temporary credentials with short TTL’s.  Within Vault, secrets are organised using an entity centric “rest-ish” style approach ensuring a given secret for a given service is stored in a single predictable location.

The use of Vault satisfies several of the design objectives:

  • enables single point management of secrets. The secrets content is stored in a single location referenced at CICD job runtime.  On the next invocation, the CICD job retrieves the latest version of the secrets content.
  • enables storing secrets in predictable locations with file system directory style path location.  The “rest-ish” approach to organising secret locations enables storing a given secret only once.  Access policies provide the mechanism to limit CICD  visibility to only those secrets required for the CICD job.

Passing Secrets to Applications

Use environment variables to pass secrets from the CICD service to the application environment.  

There are existing utilities available for populating a child process environment with Vault sourced secrets, such as vaultenv or envconsul.  This approach works well for running an application service.  However, with CICD, often there are often sets of tasks that require access to secrets information as opposed to a single command.  Using the child environment approach would require wrapping each command in a CICD job step with the env utility.  This works against the objective of introducing a secrets automation solution into existing CICD jobs without requiring substantial refactoring.  Similarly, some CICD solutions such as Jenkins provide Vault integration plugins which pre-populate the environment with secrets content.  This meets the objective of minimal CICD job refactoring, but closely couples the solution to a particular CICD service stack, reducing portability.  

With a job script oriented CICD automation stack like GitLab-CI or Travis-CI, an alternative is to insert a job step at the beginning of a series of CICD tasks that will populated the required secret values into expected environment variables.  Subsequent tasks in the job can then execute without requiring refactoring.  The decision on whether to source a particular environment variable’s content directly from the CICD job setup or from the Vault secrets store can be made by adding an optional prefix to environment variables to be sourced from the Vault secrets store.  The prefixed instance of the environment variable contains the location or path to the required secret.  Secret locations are identified using the convention /<vault-secret-path>/<secret-key>

  • enables progressive implementation due to transparency of secret sourcing. Subsequent steps continue to rely on expected environment vars
  • enables use in any toolchain that supports use of environment variables to pass information to application environment. 
  • CICD job steps not tied to a specific secrets store. An alternative secrets storage service could be supported by only requiring modification of the secret getter utility.
  • control of whether to source application environment variables from the CICD job directly or from the secrets engine is managed at the CICD job setup level as opposed to requiring CICD job refactoring to switch the content source.
  • continues the 12 Factor App approach of using environment variables to pass context to application environments.

Example Workflow

An example workflow for a CICD job designed to use environment variables for configuring an application.

Assumptions

The following are available in the CICD environment.

  • A job script oriented CICD automation stack that executes job tasks as a series of shell commands, such as GitLab-CI or Jenkins Pipelines.
  • A secrets storage engine with a python API, such as Hashicorp Vault.
  • CICD execution environment includes the get-vault-secrets-by-approle utility script.

Workflow Steps

Add a Vault secret

Add a secret to Vault at the location secret/fake-app/users/fake-users with a key/value entry of password=fake-password

Add a Vault access policy

Add a Vault policy for the CICD job (or set of CICD jobs) that includes ‘read’ access to the secret.

# cicd-fake-app-policy 
path "secret/data/fake-app/users/fake-user" {
    capabilities = ["read"]
}

path "secret/metadata/fake-app/users/fake-user" {
    capabilities = ["list"]
}

Add a Vault appRole

Add a Vault appRole linked to the new policy.  This example specifies a new appRole with an secret-id TTL of 60 days and non-renewable access tokens with a TTL of 5 minutes.  The CICD job uses the access token to read secrets.

vault write auth/approle/role/fake-role \
    secret_id_ttl=1440h \
    token_ttl=5m \
    token_max_ttl=5m \
    policies=cicd-fake-app-policy

Read the Vault approle-id

Retrieve the approle-id of the new appRole taking note of the returned approle-id.

vault read auth/approle/role/fake-role

Add a Vault appRole secret-id

Add a secret-id for the appRole, taking note of the returned secret-id

vault write -f auth/approle/role/fake-role/secret-id

Add CICD Job Steps

In the CICD job definition insert job steps to retrieve secrets values a set variables in the job execution environment. These are the steps to add in a gitlab-ci.yml CICD job.

...
script:
- get-vault-secrets-by-approle > ${VAULT_VAR_FILE}
- source ${VAULT_VAR_FILE} && rm ${VAULT_VAR_FILE}
...

The helper script get-vault-secrets-by-approle could be executed and sourced in a single step, e.g. source $(get-vault-secrets-by-approle).  However, when executed in a single statement all script output is processed by the source command and script error messages don’t get printed and captured in the job logs.  Splitting the read and environment var sourcing into 2 steps aids in troubleshooting.

Add CICD job vars for Vault access

In the CICD job configuration add Vault access environment variables.

VAULT_ADDR=https://vault.example.com:8200
VAULT_ROLE_ID=db02de05-fa39-4855-059b-67221c5c2f63
VAULT_SECRET_ID=6a174c20-f6de-a53c-74d2-6018fcceff64
VAULT_VAR_FILE=/var/tmp/vault-vars.sh

Add CICD job vars for Vault secrets

In the CICD job configuration add environment variables for the items to be sourced from vault secrets.  The secret path follows the convention of <secret-mount-path>/<secret-path>/<secret-key>

V_FAKE_PASSWORD=secret/fake-app/users/fake-user/password

Remove CICD job vars

In the CICD job configuration remove the previously used FAKE_APP_PASSWORD variable.

Execute the CICD job

Kick off the CICD job.  Any CICD job configuration variables prefixed with “V_” results in the addition of a corresponding environment variable in the job execution environment with content sourced from Vault.

Full source code can be found at:

https://github.com/datwiz/cicd-secrets-in-vault