Enforcing Terraform Policies and Standards

Basic Pipeline Flow

  1. Execute terraform plan -out tfplan
  2. Ask for user approval/denial
  3. If user approves, execute terraform apply tfplan

Creating a Terraform Policy/Enforcement

package main

import (
"encoding/json"
"flag"
"fmt"
tfjson "github.com/hashicorp/terraform-json"
"io/ioutil"
"log"
"os"
)

func main() {
tfplan := flag.String("tfplan", "plan.json", "Path to tfplan json file to be analyzed")
flag.Parse()

// Read in the tfplan json
b, err := ioutil.ReadFile(*tfplan)
if err != nil {
panic(fmt.Errorf("failed to read file: %w", err))
}
var plan tfjson.Plan
json.Unmarshal(b, &plan)

// Define the checks that will be enforced
enforcements := map[string]func(plan tfjson.Plan) []string{
"cannot define aws_vpc resources": CheckVpcDefinition,
}

// Evaluate all defined enforcements
violations := map[string][]string{}
for rule, f := range enforcements {
violations[rule] = f(plan)
}

// Print out all violations
l := log.New(os.Stderr, "", 0)
for rule, addresses := range violations {
for _, a := range addresses {
l.Printf("%s (%s)\n", rule, a)
}
}
}

// CheckVpcDefinition will validate that no aws_vpc resources
// have been defined
func CheckVpcDefinition(plan tfjson.Plan) []string {
var addresses []string
for _, resourceChange := range plan.ResourceChanges {
if resourceChange.Type == "aws_vpc" {
addresses = append(addresses, resourceChange.Address)
}
}
return addresses
}
  1. Read in the terraform plan json file specified with the tfplan flag
  2. Parse the json file into a tfjson.Plan object
  3. Defines which enforcements should be executed as a map containing a mapping of message -> func. (Message is a string that is printed with each violator that is returned by the func. The func defines a particular policy or standard and returns the address of each terraform resource that violates the defined condition)
  4. Executes all defined enforcements
  5. Prints a message to stderr for each violation containing the address of the offending resource, like:
cannot define aws_vpc resources (aws_vpc.test)
func CheckIamPolicyStar(plan tfjson.Plan) []string {
var addresses []string
for _, resourceChange := range plan.ResourceChanges {
if resourceChange.Type == "aws_iam_policy" {
addr := resourceChange.Address

var after map[string]interface{}
var ok bool
if after, ok = (resourceChange.Change.After).(map[string]interface{}); !ok {
continue
}

var p string
if p, ok = after["policy"].(string); !ok {
continue
}

var j Policy
// Unmarshal the policy that is nested as an escaped json string
json.Unmarshal([]byte(p), &j)

for _, s := range j.Statements {
if strings.ToUpper(s.Effect) == "ALLOW" {
for _, a := range s.Action {
if a == "*" {
addresses = append(addresses, addr)
}
}
}
}
}
}

return addresses
}
type Policy struct {
Statements []Statement `json:"Statement"`
}

type Statement struct {
Effect string `json:"Effect"`
Action Value `json:"Action"`
}

// Value is a custom slice of strings needed for unmarshalling IAM policies
type Value []string

// UnmarshalJSON is a custom unmarshaller that will allow a json Value to be defined either as a string or []string
func (value *Value) UnmarshalJSON(b []byte) error {

var raw interface{}
err := json.Unmarshal(b, &raw)
if err != nil {
return err
}

var p []string
// value can be string or []string, convert everything to []string
switch v := raw.(type) {
case string:
p = []string{v}
case []interface{}:
var items []string
for _, item := range v {
items = append(items, fmt.Sprintf("%v", item))
}
p = items
default:
return fmt.Errorf("invalid %s value element: allowed is only string or []string", value)
}

*value = p
return nil
}
resource "aws_iam_policy" "example" {
name = "example-policy"
description
= "This is an example policy"
policy
= <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
]
}
EOF
}

Final Pipeline Flow

  1. Execute terraform plan -out tfplan
  2. Execute terraform show -json tfplan > tfplan.json
  3. Execute app that checks if the defined Terraform complies with defined the defined enforcements: tfenforce -tfplan tfplan.json
  4. If any violations were detected then they will be printed to stdout and the CI pipeline should configured to terminate if this occurs
  5. If no violations were detected, then ask for user approval/denial
  6. If user approves, execute terraform apply tfplan

Conclusion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store