├── README.md ├── plugin.yaml ├── ssm.sh └── tests └── testchart ├── .helmignore ├── Chart.yaml ├── templates ├── _helpers.tpl └── deployment.yaml └── values.yaml /README.md: -------------------------------------------------------------------------------- 1 | # helm-ssm 2 | A low-dependency tool used to retrieves and injects secrets from AWS SSM Parameter Store. 3 | 4 | [![asciicast](https://asciinema.org/a/247712.svg)](https://asciinema.org/a/247712) 5 | 6 | 7 | ## Installation 8 | ```bash 9 | $ helm plugin install https://github.com/totango/helm-ssm 10 | ``` 11 | 12 | ## Overview 13 | This plugin provides the ability to encode AWS SSM parameter paths into your 14 | value files to store in version control or just generally less secure places. 15 | 16 | During installation or upgrade, the parameters are replaced with their actual values 17 | and passed on to Tiller. 18 | 19 | Usage: 20 | Simply use helm as you would normally, but add 'ssm' before any command, 21 | the plugin will automatically search for values with the pattern: 22 | ``` 23 | {{ssm /path/to/parameter aws-region}} 24 | ``` 25 | and replace them with their decrypted value. 26 | 27 | Optionally, if you have the same parameter for different profiles, you can set it like this: 28 | ``` 29 | {{ssm /path/to/parameter aws-region profile}} 30 | ``` 31 | 32 | 33 | >Note: You must have IAM access to the parameters you're trying to decrypt, and their KMS key. 34 | 35 | >Note #2: Wrap the template with quotes, otherwise helm will confuse the brackets for json, and will fail rendering. 36 | 37 | >Note #3: Currently, helm-ssm does not work when the value of the parameter is in the default chart values. 38 | 39 | E.g: 40 | ```bash 41 | $ helm ssm install stable/docker-registry --values value-file1.yaml -f value-file2.yaml 42 | ``` 43 | 44 | value-file1.yaml: 45 | ``` 46 | secrets: 47 | haSharedSecret: "{{ssm /mgmt/docker-registry/shared-secret us-east-1}}" 48 | htpasswd: "{{ssm /mgmt/docker-registry/htpasswd us-east-1}}" 49 | ``` 50 | 51 | ## Limitations 52 | Since helm-ssm strives to be a no-dependencies tool, it is written using only shell commands, which introduces some limitations: 53 | 54 | 1. You cant have `|` in the parameter value - helm-ssm uses `sed` to replace the placeholders with the value of the parameter, and uses `|` as the delimiter, which means the value of the parameter can't also have it. As far as I know, other special characters are allowed. 55 | 56 | ## Testing 57 | ``` 58 | $ ./ssm.sh install tests/testchart/ --debug --dry-run -f tests/testchart/values.yaml 59 | ``` 60 | -------------------------------------------------------------------------------- /plugin.yaml: -------------------------------------------------------------------------------- 1 | name: "ssm" 2 | version: "1.0.4" 3 | usage: "AWS SSM parameter injection into Helm value files for git storage" 4 | description: |- 5 | AWS SSM parameter injection in Helm value files 6 | 7 | This plugin provides the ability to encode AWS SSM parameter paths into your 8 | value files to store in version control or just generally less secure places. 9 | 10 | During installation or upgrade, the parameters are replaced with their actual values 11 | and passed on to Tiller. 12 | 13 | Usage: 14 | Simply use helm as you would normally, but add 'ssm' before any command, 15 | the plugin will automatically search for values with the pattern: 16 | 17 | {{ssm /path/to/parameter aws-region}} 18 | 19 | and replace them with their decrypted value. 20 | Note: You must have IAM access to the parameters you're trying to decrypt, and their KMS key. 21 | 22 | 23 | E.g: 24 | helm ssm install stable/docker-registry --values value-file1.yaml -f value-file2.yaml 25 | 26 | value-file1.yaml: 27 | --- 28 | secrets: 29 | haSharedSecret: {{ssm /mgmt/docker-registry/shared-secret us-east-1}} 30 | htpasswd: {{ssm /mgmt/docker-registry/htpasswd us-east-1}} 31 | --- 32 | command: "$HELM_PLUGIN_DIR/ssm.sh" 33 | useTunnel: true -------------------------------------------------------------------------------- /ssm.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | # A bunch of text colors for echoing 5 | RED='\033[0;31m' 6 | GREEN='\033[0;32m' 7 | BLUE='\033[0;34m' 8 | YELLOW='\033[1;33m' 9 | NOC='\033[0m' 10 | 11 | # Checks if a value exists in an array 12 | # Usage: elementIn "some_value" "${VALUES[@]}"; [[ #? -eq 0 ]] && echo "EXISTS!" || echo "DOESNT EXIST! :(" 13 | function elementIn () { 14 | local e match="$1" 15 | shift 16 | for e; do [[ "$e" == "$match" ]] && return 0; done 17 | return 1 18 | } 19 | 20 | function printUsage () { 21 | set -e 22 | cat <&2 61 | exit 1 62 | fi 63 | 64 | 65 | # get the first command (install\list\template\etc...) 66 | cmd="$1" 67 | 68 | # "helm ssm/helm ssm help/helm ssm -h/helm ssm --help" 69 | if [[ $# -eq 0 || "$cmd" == "help" || "$cmd" == "-h" || "$cmd" == "--help" ]]; then 70 | printUsage 71 | fi 72 | 73 | # if the command is not "install" or "upgrade", or just a single command (no value files is a given in this case), pass the args to the regular helm command 74 | if [[ $# -eq 1 || ( "$cmd" != "install" && "$cmd" != "upgrade" && "$cmd" != "template") ]]; then 75 | set +e # disable fail-fast 76 | helm "$*" 77 | EXIT_CODE=$? 78 | 79 | if [[ ${EXIT_CODE} -ne 0 ]]; then 80 | echo -e "${RED}[SSM]${NOC} Helm exited with a non 0 code - this is most likely not a problem with the SSM plugin, but a problem with Helm itself." >&2 81 | fi 82 | 83 | exit ${EXIT_CODE} # exit with the same error code as the command 84 | fi 85 | 86 | 87 | VALUE_FILES=() # An array of paths to value files 88 | OPTIONS=() # An array of all the other options given 89 | while [[ "$#" -gt 0 ]] 90 | do 91 | case "$1" in 92 | -h|--help) 93 | echo "usage!" # TODO proper usage 94 | exit 0 95 | ;; 96 | -f|--values) 97 | if [ $# -gt 1 ]; then # if we werent given just an empty '-f' option 98 | VALUE_FILES+=($2) # then add the path to the array 99 | fi 100 | ;; 101 | *) 102 | # we go over each options, and if the option isnt a value file, we add it to the options array 103 | set +e # we turn off fast-fail because the check of if the array contains a value returns exit code 0 or 1 depending on the result 104 | elementIn "$1" "${VALUE_FILES[@]}" 105 | [[ $? -eq 1 ]] && OPTIONS+=($1) 106 | set -e # when we're finished with the check, we turn on fast-fail 107 | ;; 108 | esac 109 | shift 110 | done 111 | 112 | echo -e "${GREEN}[SSM]${NOC} Options: ${OPTIONS[@]}" 113 | echo -e "${GREEN}[SSM]${NOC} Value files: ${VALUE_FILES[@]}" 114 | 115 | set +e # we disable fail-dast because we want to give the user a proper error message in case we cant read the value file 116 | MERGED_TEXT="" 117 | for FILEPATH in "${VALUE_FILES[@]}"; do 118 | echo -e "${GREEN}[SSM]${NOC} Reading ${FILEPATH}" 119 | 120 | if [[ ! -f ${FILEPATH} ]]; then 121 | echo -e "${RED}[SSM]${NOC} Error: open ${FILEPATH}: no such file or directory" >&2 122 | exit 1 123 | fi 124 | 125 | VALUE=$(cat ${FILEPATH} 2> /dev/null) # read the content of the values file silently (without outputing an error in case it fails) 126 | EXIT_CODE=$? 127 | 128 | if [[ ${EXIT_CODE} -ne 0 ]]; then 129 | echo -e "${RED}[SSM]${NOC} Error: open ${FILEPATH}: failed to read contents" >&2 130 | exit 1 131 | fi 132 | 133 | VALUE=$(echo -e "${VALUE}" | sed s/\%/\%\%/g) # we turn single % to %% to escape percent signs 134 | printf -v MERGED_TEXT "${MERGED_TEXT}\n${VALUE}" # We concat the files together with a newline in between using printf and put output into variable MERGED_TEXT 135 | done 136 | 137 | PARAMETERS=$(echo -e "${MERGED_TEXT}" | grep -Eo "\{\{ssm [^\}]+\}\}") # Look for {{ssm /path/to/param us-east-1}} patterns, delete empty lines 138 | PARAMETERS_LENGTH=$(echo "${PARAMETERS}" | grep -v '^$' | wc -l | xargs) 139 | if [ "${PARAMETERS_LENGTH}" != 0 ]; then 140 | echo -e "${GREEN}[SSM]${NOC} Found $(echo "${PARAMETERS}" | grep -v '^$' | wc -l | xargs) parameters" 141 | echo -e "${GREEN}[SSM]${NOC} Parameters: \n${PARAMETERS[@]}" 142 | else 143 | echo -e "${GREEN}[SSM]${NOC} No parameters were found, continuing..." 144 | fi 145 | echo -e "===============================================" 146 | 147 | 148 | set +e 149 | # using 'while' instead of 'for' allows us to use newline as a delimiter instead of a space 150 | while read -r PARAM_STRING; do 151 | [ -z "${PARAM_STRING}" ] && continue # if parameter is empty for some reason 152 | 153 | CLEANED_PARAM_STRING=$(echo ${PARAM_STRING:2} | rev | cut -c 3- | rev) # we cut the '{{' and '}}' at the beginning and end 154 | PARAM_PATH=$(echo ${CLEANED_PARAM_STRING:2} | cut -d' ' -f 2) # {{ssm */param/path* us-east-1}} 155 | REGION=$(echo ${CLEANED_PARAM_STRING:2} | cut -d' ' -f 3) # {{ssm /param/path *us-east-1*}} 156 | PROFILE=$(echo ${CLEANED_PARAM_STRING:2} | cut -d' ' -f 4) # {{ssm /param/path us-east-1 *production*}} 157 | if [[ -n ${PROFILE} ]]; then 158 | PROFILE_PARAM="--profile ${PROFILE}" 159 | fi 160 | PARAM_OUTPUT="$(aws ssm get-parameter --with-decryption --name ${PARAM_PATH} --output text --query Parameter.Value --region ${REGION} $PROFILE_PARAM 2>&1)" # Get the parameter value or error message 161 | EXIT_CODE=$? 162 | 163 | if [[ ${EXIT_CODE} -ne 0 ]]; then 164 | echo -e "${RED}[SSM]${NOC} Error: Could not get parameter: ${PARAM_PATH}. AWS cli output: ${PARAM_OUTPUT}" >&2 165 | exit 1 166 | fi 167 | 168 | SECRET_TEXT="$(echo -e "${PARAM_OUTPUT}" | sed -e 's/[]\&\/$*.^[]/\\&/g')" 169 | MERGED_TEXT=$(echo -e "${MERGED_TEXT}" | sed "s|${PARAM_STRING}|${SECRET_TEXT}|g") 170 | sleep 0.5 # very basic rate limits 171 | done <<< "${PARAMETERS}" 172 | 173 | set +e 174 | echo -e "${MERGED_TEXT}" | helm "${OPTIONS[@]}" --values - 175 | EXIT_CODE=$? 176 | if [[ ${EXIT_CODE} -ne 0 ]]; then 177 | echo -e "${RED}[SSM]${NOC} Helm exited with a non 0 code - this is most likely not a problem with the SSM plugin, but a problem with Helm itself." >&2 178 | exit ${EXIT_CODE} 179 | fi 180 | -------------------------------------------------------------------------------- /tests/testchart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /tests/testchart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: A Helm chart for Kubernetes 4 | name: testchart 5 | version: 0.1.0 6 | -------------------------------------------------------------------------------- /tests/testchart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "testchart.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "testchart.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "testchart.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | -------------------------------------------------------------------------------- /tests/testchart/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "testchart.fullname" . }} 5 | spec: 6 | replicas: {{ .Values.replicaCount }} 7 | selector: 8 | matchLabels: 9 | app.kubernetes.io/name: {{ include "testchart.name" . }} 10 | template: 11 | spec: 12 | containers: 13 | - name: {{ .Chart.Name }} 14 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 15 | env: 16 | - name: A_VERY_SECRET_PASSWORD 17 | value: {{ .Values.secret | quote }} 18 | 19 | -------------------------------------------------------------------------------- /tests/testchart/values.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 1 2 | 3 | image: 4 | repository: nginx 5 | tag: stable 6 | pullPolicy: IfNotPresent 7 | 8 | nameOverride: "" 9 | fullnameOverride: "" 10 | 11 | secret: "{{ssm /test/parameter us-east-1}}" 12 | 13 | 14 | --------------------------------------------------------------------------------