├── .gitignore ├── .travis.yml ├── LICENSE.md ├── Makefile ├── README.md ├── apps └── confidential │ └── main.go ├── aws ├── credentials.go ├── ssm.go └── ssm_test.go ├── commands ├── common.go ├── exec.go ├── exec_test.go └── output.go ├── environment ├── environment.go └── environment_test.go └── examples └── cloudformation ├── example-3-cloudformation.yml ├── example-4-cloudformation.yml └── example-5-cloudformation.yml /.gitignore: -------------------------------------------------------------------------------- 1 | apps/confidential/confidential -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.x 5 | - 1.6 6 | - 1.7.x 7 | - master 8 | 9 | install: go get -t ./... 10 | 11 | script: go test -v ./... -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2017 Niklas Lindblad 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY = confidential 2 | GOARCH = amd64 3 | DIR = apps/confidential 4 | VERSION = 0.1.0 5 | 6 | .PHONY: linux 7 | linux: 8 | cd ${DIR} && GOOS=linux GOARCH=${GOARCH} go build -o ${BINARY} . ; \ 9 | 10 | .PHONY: darwin 11 | darwin: 12 | cd ${DIR} && GOOS=darwin GOARCH=${GOARCH} go build -o ${BINARY} . ; \ 13 | 14 | .PHONY: windows 15 | windows: 16 | cd ${DIR} && GOOS=windows GOARCH=${GOARCH} go build -o ${BINARY}.exe . ; \ 17 | 18 | .PHONY: debian 19 | debian: linux 20 | fpm -s dir -t deb -n $(BINARY) -v $(VERSION) --prefix /usr/local/bin -C ${DIR} ${BINARY} 21 | 22 | .PHONY: rpm 23 | rpm: linux 24 | fpm --rpm-os linux -s dir -t rpm -n $(BINARY) -v $(VERSION) --prefix /usr/local/bin -C ${DIR} ${BINARY} 25 | rpm --addsign confidential-${VERSION}-1.x86_64.rpm 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/nlindblad/confidential.svg?branch=master)](https://travis-ci.org/nlindblad/confidential) 2 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/c5195a16de6f455986b13a5ff04388d3)](https://www.codacy.com/app/niklas/confidential?utm_source=github.com&utm_medium=referral&utm_content=nlindblad/confidential&utm_campaign=Badge_Grade) 3 | # confidential (working title) 4 | 5 | Export parameters from [AWS Systems Manager Parameters](http://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-working.html) as environment variables. 6 | 7 | See [some examples](#examples) of common use cases. 8 | 9 | ## Why I wrote this? 10 | 11 | Configuration management, specifically secrets management tends to get complicated. After having been through several projects, both in my spare time and at work, using solutions such as [Ansible Vault](https://docs.ansible.com/ansible/2.4/vault.html), private [AWS CodeCommit](https://aws.amazon.com/codecommit/) repositories or [Amazon KMS](https://aws.amazon.com/kms/) encrypted configuration files in [Amazon S3](https://aws.amazon.com/s3/), I was looking for something simpler, while still maintaining a high level of security. 12 | 13 | I deemed self-hosted solution, such as [Hashicorp Vault](https://www.vaultproject.io/) (and the other solutions listed on [this Hashicorp Vault vs. Other Software](https://www.vaultproject.io/intro/vs/index.html) page) too time consuming to set up and maintain. 14 | 15 | Luckily, Amazon Web Services have [been busy improving their Amazon EC2 Systems Manager Parameter Store](https://aws.amazon.com/blogs/mt/amazon-ec2-systems-manager-parameter-store-adds-support-for-parameter-versions/) in 2017 and it now supports both seamless [Amazon KMS](https://aws.amazon.com/kms/) encryption and versioning of parameters. 16 | 17 | ## Getting Started 18 | 19 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system. 20 | 21 | ### Prerequisites 22 | 23 | Confidential is written in [Go](https://golang.org) and can be run as a single binary, no language specific requirements are needed. 24 | 25 | It is designed to run on the GNU/Linux, macOS, and Windows operating systems. Other operating systems will probably work as long as you can compile a Go binary on them. 26 | 27 | ### Installing 28 | 29 | Make sure you have [Go installed](https://golang.org/doc/install) and that [the `$GOPATH` is set correctly](https://github.com/golang/go/wiki/SettingGOPATH). 30 | 31 | ### Build binary 32 | 33 | ``` 34 | go get github.com/nlindblad/confidential/apps/confidential 35 | cd $GOPATH/src/github.com/nlindblad/confidential/apps/confidential 36 | go build 37 | ``` 38 | 39 | Or if you have cloned this repository: 40 | 41 | ``` 42 | make PLATFORM 43 | ``` 44 | 45 | where `PLATFORM` is one of `linux`, `darwin` or `windows`. 46 | 47 | ### Run 48 | 49 | ``` 50 | ./confidential --help 51 | ``` 52 | 53 | And you should see: 54 | 55 | ``` 56 | NAME: 57 | confidential - Export parameters from AWS Systems Manager Parameters as environment variables 58 | 59 | USAGE: 60 | confidential [global options] command [command options] [arguments...] 61 | 62 | VERSION: 63 | 0.0.0 64 | 65 | AUTHOR: 66 | Niklas Lindblad 67 | 68 | COMMANDS: 69 | exec, e retrieve environment variables and execute command with an updated environment 70 | output, o retrieve and atomically output environment variables to a file 71 | help, h Shows a list of commands or help for one command 72 | 73 | GLOBAL OPTIONS: 74 | --forwarded-profile value AWS profile to forward credentials for in the created environment [$AWS_FORWARDED_PROFILE] 75 | --prefix value Amazon SSM parameter prefix 76 | --profile value AWS profile to use when calling Amazon SSM [$AWS_PROFILE] 77 | --region value AWS region e.g. eu-west-1 [$AWS_REGION] 78 | --help, -h show help 79 | --version, -v print the version 80 | ``` 81 | 82 | ## Running the tests 83 | 84 | Tests use the excellent Go [stretchr/testify](https://github.com/stretchr/testify) package. 85 | 86 | ``` 87 | go test -v ./... 88 | ``` 89 | 90 | ## Deployment 91 | 92 | Ideally, check out the source code (either via Git or `go get github.com/nlindblad/confidential/apps/confidential`) and make yourself familiar with it and make sure you trust it to manage the most sensitive aspects of your application. 93 | 94 | For each release, there are also packages for Debian/Ubuntu and CentOS/Amazon Linux/Red Hat Linux on [the releases page](https://github.com/nlindblad/confidential/releases). 95 | 96 | The CentOS/Amazon Linux/Red Hat Linux RPM package is signed with the GPG key with ID [A4847C36](https://keybase.io/nlindblad), the same key used by the original author for signing each Git commit in this repository. 97 | 98 | ### Self built binary 99 | 100 | Simply copy the `confidential` binary to somewhere on your server, e.g. `/usr/local/bin`. 101 | 102 | ### Debian/Ubuntu 103 | 104 | ``` 105 | # wget https://github.com/nlindblad/confidential/releases/download/v0.1.0/confidential_0.1.0_amd64.deb 106 | ... 107 | # dpkg -i confidential_0.1.0_amd64.deb 108 | ... 109 | # which confidential 110 | /usr/local/bin/confidential 111 | ``` 112 | 113 | ### CentOS/Amazon Linux/Red Hat Linux 114 | 115 | ``` 116 | # curl -L https://github.com/nlindblad/confidential/releases/download/v0.1.0/confidential-0.1.0-1.x86_64.rpm --output confidential-0.1.0-1.x86_64.rpm 117 | ... 118 | # rpm -i confidential-0.1.0-1.x86_64.rpm 119 | warning: confidential-0.1.0-1.x86_64.rpm: Header V4 RSA/SHA512 Signature, key ID a4847c36: NOKEY 120 | # which confidential 121 | /usr/local/bin/confidential 122 | ``` 123 | 124 | ## Examples 125 | 126 | The machine needs to have the following AWS IAM permissions: 127 | 128 | - `kms:Decrypt` on the relevant [Amazon KMS](https://aws.amazon.com/kms/) key used to encrypt sensitive parameters. 129 | - `ssm:GetParametersByPath` on the relevant resource: `arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/` (**note** there should be no trailing slash or wildcards) 130 | 131 | [AWS Systems Manager Parameters](http://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-working.html) names are translated into environment variable compatible names using the following logic: 132 | 133 | 1. All disallowed characters are omitted. Allowed characters are `-`, `.`, `_`, `a-z`, `A-Z` and `0-9`. 134 | 2. Any non-alphanumerical character (not `a-z`, `A-Z` or `0-9`) is converted to an underscore (`_`). 135 | 136 | For example: 137 | 138 | `/my-prefix/database.password` becomes `DATABASE_PASSWORD` (as would `/my-prefix/database/password`). 139 | 140 | `/my-prefix/bar` simply becomes `BAR` 141 | 142 | 143 | ### :whale2: Use with Docker and systemd services: 144 | 145 | A handy way of running Docker containers supervised by systemd is to create a unit (service) using the [`systemd-docker` wrapper](https://github.com/ibuildthecloud/systemd-docker): 146 | 147 | ``` 148 | [Unit] 149 | Description=My service 150 | Requires=docker.service 151 | After=docker.service 152 | 153 | [Service] 154 | TimeoutStartSec=0 155 | ExecStartPre=/usr/local/bin/confidential --region eu-west-1 --prefix /my-service/prod output --env-file /etc/my-service/prod.env 156 | ExecStartPre=/usr/bin/docker pull username/image-name:latest 157 | ExecStart=/usr/local/bin/systemd-docker --cgroups name=systemd run \ 158 | --name %n \ 159 | --env-file /etc/my-service/prod.env \ 160 | ... Add other Docker run flags here ... 161 | -d username/image-name:latest 162 | ExecStop=/usr/bin/docker stop %n 163 | ExecStopPost=/usr/bin/docker rm -f %n 164 | Restart=always 165 | RestartSec=10s 166 | Type=notify 167 | NotifyAccess=all 168 | 169 | [Install] 170 | WantedBy=default.target 171 | ``` 172 | 173 | The following service will run `username/image-name` as a service which will get restarted if it falls over. 174 | 175 | Every time the service is started/restarted, it runs the two `ExecStartPre` steps: 176 | 177 | 1. Uses confidential to get the latest environment variables from [AWS Systems Manager Parameters](http://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-working.html) in the `eu-west-1` AWS Region and writes them to the file `/etc/my-service/prod.env` in a format that Docker understands 178 | 179 | 2. Pulls down the latest version of the `username/image-name` Docker image 180 | 181 | This ensures that the service is always running using the latest published Docker image and that any configuration changes are picked up automatically. 182 | 183 | Managing the environment variables for the service is now done within the `/my-service/prod` namespace in [AWS Systems Manager Parameters](http://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-working.html). 184 | 185 | ### :horse: Use with generic systemd services: 186 | 187 | The `EnvironmentFile` directive can be used to expose the retrieved environment variable to any kind of executable running as a systemd service: 188 | 189 | ``` 190 | [Unit] 191 | Description=Service 192 | After=syslog.target network.target remote-fs.target nss-lookup.target 193 | 194 | [Service] 195 | Type=forking 196 | PIDFile=/run/my-service.pid 197 | ExecStartPre=/usr/local/bin/confidential --region eu-west-1 --prefix /my-service/prod output --env-file /etc/my-service/prod.env 198 | EnvironmentFile=-/etc/my-service/prod.env 199 | ExecStart=/usr/local/bin/my-service --flag=something --foo=bar 200 | ExecReload=/bin/kill -s HUP $MAINPID 201 | ExecStop=/bin/kill -s QUIT $MAINPID 202 | PrivateTmp=true 203 | 204 | [Install] 205 | WantedBy=multi-user.target 206 | ``` 207 | 208 | Every time the service is started/restarted, it runs the `ExecStartPre` steps and populates `/etc/my-service/prod.env` and includes it using the `EnvironmentFile` directive (*note* the `-` before the filename). 209 | 210 | Managing the environment variables for the service is now done within the `/my-service/prod` namespace in [AWS Systems Manager Parameters](http://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-working.html) in the `eu-west-1` AWS Region. 211 | 212 | ### :cloud: Give EC2 hosts permissions to access specific parameters: 213 | 214 | See full CloudFormation template: [examples/cloudformation/example-3-cloudformation.yml](examples/cloudformation/example-3-cloudformation.yml) 215 | 216 | ### :telescope: Create an IAM role with permissions to access specific parameters: 217 | 218 | See full CloudFormation template: [examples/cloudformation/example-4-cloudformation.yml](examples/cloudformation/example-4-cloudformation.yml) 219 | 220 | Creates a dedicated IAM user and access keys that is allowed to decrypt and retrieve parameters with a specific prefix. 221 | 222 | *Note*: Some other tools using [AWS Systems Manager Parameters](http://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-working.html) use a mix of `ssm:DescribeParameters` and `ssm:GetParameters`, which makes it hard to create fine grained acess control, especially when iterating parameters requires permissions to describe **all** parameters. 223 | 224 | ### :pencil2: Create an IAM role with permissions to set specific parameters: 225 | 226 | See full CloudFormation template: [examples/cloudformation/example-5-cloudformation.yml](examples/cloudformation/example-5-cloudformation.yml) 227 | 228 | Creates a dedicated IAM user and access keys that is allowed to encrypt and set parameters with a specific prefix, but not retrieve or decrypt. 229 | 230 | *Example usage*: 231 | 232 | ``` 233 | aws --profile ssm put-parameter --name '/' --type "SecureString" --value '' 234 | ``` 235 | 236 | 237 | ### :shell: Run arbitrary executable with an environment populated by confidential: 238 | 239 | The simplest example of this is running the `/usr/bin/env` utility and print out the environment variables that are accessible to the newly invoked process: 240 | 241 | ``` 242 | /usr/local/bin/confidential --region eu-west-1 --prefix /my-service/prod exec -- env 243 | ``` 244 | 245 | ### :octopus: Use with supervisord: 246 | 247 | ``` 248 | [program:my-service] 249 | command=/usr/local/bin/confidential --region eu-west-1 --prefix /my-service/prod exec -- /usr/local/bin/my-service --flag=something --foo=bar 250 | directory=/tmp 251 | autostart=true 252 | autorestart=true 253 | startretries=3 254 | stdout_logfile=/tmp/my-service.log 255 | stderr_logfile=/tmp/my-service.err.log 256 | user=username 257 | ``` 258 | 259 | ### :card_index: Use specific AWS profile from `~/.aws/credentials` 260 | 261 | By default, the AWS SDK for Go will [automatically look for AWS credentials in a couple of pre-defined places](https://github.com/aws/aws-sdk-go#configuring-credentials). 262 | 263 | If you are using the standard `~/.aws/credentials` (used by the standard [AWS CLI tool](https://aws.amazon.com/cli/)), you can specify multiple sections with different credentials: 264 | 265 | ``` 266 | [default] 267 | aws_access_key_id = AKIAPEIPJKJSOJ267 268 | aws_secret_access_key = XXXXXXXXXXXXXXXXXXX 269 | 270 | [parameters-read] 271 | aws_access_key_id = AKIABCDEFGH12345 272 | aws_secret_access_key = XXXXXXXXXXXXXXXXXXX 273 | ``` 274 | 275 | Using the `--profile` flag, you can specify that you want to use the `parameters-read` profile instead of the `default` one (which would get picked up by the AWS SDK for Go): 276 | 277 | ``` 278 | /usr/local/bin/confidential --profile parameters-read --region eu-west-1 --prefix /my-service/prod output --env-file /etc/my-service/prod.env 279 | ``` 280 | 281 | ### :fast_forward: Forward AWS credentials from `~/.aws/credentials` to new environment 282 | 283 | It is possible to forward AWS credentials from `~/.aws/credentials` for a given profile to the new enviromment using the `--forwarded-profile` flag. 284 | 285 | Given a `~/.aws/credentials` file: 286 | 287 | ``` 288 | [default] 289 | aws_access_key_id = AKIAPEIPJKJSOJ267 290 | aws_secret_access_key = XXXXXXXXXXXXXXXXXXX 291 | 292 | [parameters-read] 293 | aws_access_key_id = AKIABCDEFGH12345 294 | aws_secret_access_key = XXXXXXXXXXXXXXXXXXX 295 | 296 | [my-service] 297 | aws_access_key_id = AKIAHIHIIW233445 298 | aws_secret_access_key = XXXXXXXXXXXXXXXXXXX 299 | ``` 300 | 301 | You can use the the AWS credentials for the `parameters-read` profile to retrieve the parameters from [AWS Systems Manager Parameters](http://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-working.html) and forward the AWS credentials for the `my-service` profile using: 302 | 303 | ``` 304 | /usr/local/bin/confidential --profile parameters-read --forwarded-profile my-service --region eu-west-1 --prefix /my-service/prod output --env-file /etc/my-service/prod.env 305 | ``` 306 | 307 | In the above example, `/etc/my-service/prod.env` would contain all parameters retrieved from [AWS Systems Manager Parameters](http://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-working.html) in the `eu-west-1` AWS Region in addition to: 308 | 309 | ``` 310 | AWS_ACCESS_KEY_ID=AKIAHIHIIW233445 311 | AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXXX 312 | AWS_SESSION_TOKEN= 313 | ``` 314 | 315 | ## Built With 316 | 317 | * [aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) - AWS SDK for Go 318 | * [urfave/cli](https://github.com/urfave/cli) - Command line interface helpers 319 | * [dchest/safefile](https://github.com/dchest/safefile) - Safe "atomic" saving of files 320 | 321 | ## Versioning 322 | 323 | Uses [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/nlindblad/confidential/tags). 324 | 325 | ## Authors 326 | 327 | * **Niklas Lindblad** - *Initial work* 328 | 329 | ## License 330 | 331 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 332 | 333 | ## Acknowledgments 334 | 335 | * [Sjeanpierre/param_api](https://github.com/Sjeanpierre/param_api) provided a great starting point for using the Amazon SSM API in Go 336 | * [gurusi/systemd-make-environment](https://github.com/gurusi/systemd-make-environment) for mentioning some common gotchas with `EnvironmentFile` and `ExecStartPre` with `systemd` 337 | * [Storing Secrets with AWS ParameterStore](https://typicalrunt.me/2017/04/07/storing-secrets-with-aws-parameterstore/) provided a great starting point for the CloudFormation templates 338 | * [segmentio/chamber](https://github.com/segmentio/chamber) for a nice way of implementing the `exec` command 339 | -------------------------------------------------------------------------------- /apps/confidential/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "sort" 7 | 8 | "github.com/urfave/cli" 9 | 10 | 11 | "github.com/nlindblad/confidential/commands" 12 | ) 13 | 14 | func main() { 15 | app := cli.NewApp() 16 | 17 | app.Flags = []cli.Flag { 18 | cli.StringFlag{ 19 | Name: "region", 20 | Usage: "AWS region e.g. eu-west-1", 21 | EnvVar: "AWS_REGION", 22 | }, 23 | cli.StringFlag{ 24 | Name: "prefix", 25 | Usage: "Amazon SSM parameter prefix", 26 | }, 27 | cli.StringFlag{ 28 | Name: "profile", 29 | Usage: "AWS profile to use when calling Amazon SSM", 30 | EnvVar: "AWS_PROFILE", 31 | }, 32 | cli.StringFlag{ 33 | Name: "forwarded-profile", 34 | Usage: "AWS profile to forward credentials for in the created environment", 35 | EnvVar: "AWS_FORWARDED_PROFILE", 36 | }, 37 | } 38 | 39 | app.Version = "0.1.0" 40 | app.Usage = "Export parameters from AWS Systems Manager Parameters as environment variables" 41 | 42 | app.Name = "confidential" 43 | app.Authors = []cli.Author{ 44 | { 45 | Name: "Niklas Lindblad", 46 | Email: "niklas@lindblad.info", 47 | }, 48 | } 49 | 50 | app.Commands = commands.GetCommands() 51 | 52 | sort.Sort(cli.FlagsByName(app.Flags)) 53 | sort.Sort(cli.CommandsByName(app.Commands)) 54 | 55 | if err := app.Run(os.Args); err != nil { 56 | log.Fatal(err) 57 | } 58 | } -------------------------------------------------------------------------------- /aws/credentials.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | "path/filepath" 8 | 9 | //"github.com/aws/aws-sdk-go/aws/credentials" 10 | "github.com/aws/aws-sdk-go/aws/credentials" 11 | ) 12 | 13 | // UserHomeDir returns the home directory for the user the process is running under 14 | func UserHomeDir() string { 15 | if runtime.GOOS == "windows" { // Windows 16 | return os.Getenv("USERPROFILE") 17 | } 18 | 19 | // *nix 20 | return os.Getenv("HOME") 21 | } 22 | 23 | // Get AWS credentials for a specific profile in ~/.aws/credentials 24 | func GetAwsCredentialsForProfile(profile string) (*credentials.Credentials, error) { 25 | awsCredentialsFilename := filepath.Join(UserHomeDir(), ".aws", "credentials") 26 | if _, err := os.Stat(awsCredentialsFilename); !os.IsNotExist(err) { 27 | credentials := credentials.NewSharedCredentials(awsCredentialsFilename, profile) 28 | return credentials, nil 29 | } 30 | return nil, fmt.Errorf("could not read AWS profiles from %s", awsCredentialsFilename) 31 | } 32 | -------------------------------------------------------------------------------- /aws/ssm.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/service/ssm" 5 | "github.com/aws/aws-sdk-go/aws" 6 | "github.com/aws/aws-sdk-go/aws/session" 7 | "github.com/aws/aws-sdk-go/aws/credentials" 8 | ) 9 | 10 | // Wrapped Amazon SSM client 11 | type SsmClient struct { 12 | client *ssm.SSM 13 | } 14 | 15 | // Decrypted parameter (without prefix) from Amazon SSM 16 | type DecryptedParameter struct { 17 | Name string 18 | Value string 19 | } 20 | 21 | // List of decrypted parameters (without prefix) from Amazon SSM 22 | type DecryptedParameters []DecryptedParameter 23 | 24 | // Create new wrapped Amazon SSM client 25 | func NewClient(awsRegion string) (*SsmClient, error) { 26 | session, err := session.NewSession(&aws.Config{ 27 | Region: aws.String(awsRegion), 28 | }) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return &SsmClient{ssm.New(session)}, nil 34 | } 35 | 36 | // Create new wrapped Amazon SSM client 37 | func NewClientWithCredentials(awsRegion string, credentials *credentials.Credentials) (*SsmClient, error) { 38 | session, err := session.NewSession(&aws.Config{ 39 | Region: aws.String(awsRegion), 40 | Credentials: credentials, 41 | }) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return &SsmClient{ssm.New(session)}, nil 47 | } 48 | 49 | func (s SsmClient) paramListPaginated(prefix string, nextToken *string) ([]ssm.Parameter, *string, error) { 50 | var parameters []ssm.Parameter 51 | 52 | getParametersByPathInput := &ssm.GetParametersByPathInput{ 53 | NextToken: nextToken, 54 | Path: &prefix, 55 | Recursive: aws.Bool(true), 56 | WithDecryption: aws.Bool(true), 57 | } 58 | 59 | result, err := s.client.GetParametersByPath(getParametersByPathInput) 60 | if err != nil { 61 | return nil, nil, err 62 | } 63 | 64 | for _, parameter := range result.Parameters { 65 | parameters = append(parameters, *parameter) 66 | } 67 | 68 | return parameters, result.NextToken, nil 69 | } 70 | 71 | func (s SsmClient) paramList(prefix string) (*[]ssm.Parameter, error) { 72 | parameters, nextToken, err := s.paramListPaginated(prefix, nil) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | for nextToken != nil { 78 | var additionalParameters []ssm.Parameter 79 | additionalParameters, nextToken, err = s.paramListPaginated(prefix, nextToken) 80 | if err != nil { 81 | return nil, err 82 | } 83 | for _, parameter := range additionalParameters { 84 | parameters = append(parameters, parameter) 85 | } 86 | } 87 | 88 | return ¶meters, nil 89 | } 90 | 91 | func (s SsmClient) WithPrefix(prefix string) (DecryptedParameters, error) { 92 | var parameters DecryptedParameters 93 | 94 | retrievedParameters, err := s.paramList(prefix) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | for _, parameter := range *retrievedParameters { 100 | nameWithoutPrefix := string([]rune(*parameter.Name)[len(prefix)+1:]) 101 | parameters = append(parameters, DecryptedParameter{nameWithoutPrefix, *parameter.Value}) 102 | } 103 | 104 | return parameters, nil 105 | } -------------------------------------------------------------------------------- /aws/ssm_test.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "testing" 5 | "github.com/aws/aws-sdk-go/service/ssm" 6 | "github.com/aws/aws-sdk-go/aws/request" 7 | "github.com/aws/aws-sdk-go/aws/session" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/aws/aws-sdk-go/aws" 10 | "strings" 11 | ) 12 | 13 | type mockSsmParameterStore struct { 14 | parameters map[string]string 15 | } 16 | 17 | func newMockSsmParameterStore() (*mockSsmParameterStore) { 18 | parameters := make(map[string]string) 19 | return &mockSsmParameterStore{parameters} 20 | } 21 | 22 | func (msp *mockSsmParameterStore) set(key string, value string) { 23 | msp.parameters[key] = value 24 | } 25 | 26 | func (msp *mockSsmParameterStore) get(key string) (*ssm.Parameter) { 27 | return &ssm.Parameter{Name: aws.String(key), Value: aws.String(msp.parameters[key])} 28 | } 29 | 30 | func (msp *mockSsmParameterStore) getParametersByPath(path *string) ([]*ssm.Parameter) { 31 | var parameters []*ssm.Parameter 32 | for key := range msp.parameters { 33 | if strings.HasPrefix(key, *path) { 34 | parameters = append(parameters, msp.get(key)) 35 | } 36 | } 37 | 38 | return parameters 39 | } 40 | 41 | func TestSsmClient_WithPrefix(t *testing.T) { 42 | parameterStore := newMockSsmParameterStore() 43 | parameterStore.set("/test/prod/secret_key", "value1") 44 | parameterStore.set("/test/prod/database_password", "value2") 45 | parameterStore.set("/test/prod/cookie_secret", "value3") 46 | parameterStore.set("/test/dev/cookie_secret", "value4") 47 | parameterStore.set("/test/dev/cookie_secret", "value5") 48 | 49 | expectedParameters := make(map[string]string) 50 | expectedParameters["secret_key"] = *parameterStore.get("/test/prod/secret_key").Value 51 | expectedParameters["database_password"] = *parameterStore.get("/test/prod/database_password").Value 52 | expectedParameters["cookie_secret"] = *parameterStore.get("/test/prod/cookie_secret").Value 53 | 54 | client := SsmClient{ssm.New(session.Must(session.NewSession()))} 55 | 56 | client.client.Handlers.Clear() 57 | client.client.Handlers.Send.PushBack(func(r *request.Request) { 58 | getParameterByPathInput, inputOk := r.Params.(*ssm.GetParametersByPathInput) 59 | getParameterByPathOutput, outputOk := r.Data.(*ssm.GetParametersByPathOutput) 60 | if inputOk && outputOk { 61 | path := getParameterByPathInput.Path 62 | getParameterByPathOutput.Parameters = parameterStore.getParametersByPath(path) 63 | } 64 | }) 65 | 66 | retrievedParameters, _ := client.WithPrefix("/test/prod") 67 | 68 | assert.Equal(t, len(expectedParameters), len(retrievedParameters)) 69 | for _, retrievedParameter := range retrievedParameters { 70 | assert.Equal(t, expectedParameters[retrievedParameter.Name], retrievedParameter.Value) 71 | } 72 | } -------------------------------------------------------------------------------- /commands/common.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/urfave/cli" 7 | 8 | "github.com/nlindblad/confidential/environment" 9 | "github.com/nlindblad/confidential/aws" 10 | ) 11 | 12 | // Get a mandatory command line flag and error if it does not exist 13 | func GetMandatoryFlag(c *cli.Context, name string) (*string, error) { 14 | value := "" 15 | if c.GlobalIsSet(name) { 16 | value = c.GlobalString(name) 17 | } else { 18 | value = c.String(name) 19 | } 20 | 21 | if value == "" { 22 | return nil, fmt.Errorf("required flag --%s not provided", name) 23 | } 24 | 25 | return &value, nil 26 | } 27 | 28 | func EnvironmentForAwsCredentialsProfile(profile string) (*environment.Environment, error) { 29 | environmentWithAwsCredentials := environment.NewEnvironment() 30 | 31 | credentials, err:= aws.GetAwsCredentialsForProfile(profile) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | value, err := credentials.Get() 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | environmentWithAwsCredentials.Add(environment.Variable{"AWS_ACCESS_KEY_ID", value.AccessKeyID}) 42 | environmentWithAwsCredentials.Add(environment.Variable{"AWS_SECRET_ACCESS_KEY", value.SecretAccessKey}) 43 | environmentWithAwsCredentials.Add(environment.Variable{"AWS_SESSION_TOKEN", value.SessionToken}) 44 | 45 | return environmentWithAwsCredentials, nil 46 | } 47 | 48 | // Create new wrapped Amazon SSM client from CLI context 49 | func NewClientFromContext(c *cli.Context) (*aws.SsmClient, error) { 50 | region, err := GetMandatoryFlag(c, "region") 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | if c.GlobalIsSet("profile") { 56 | profile := c.GlobalString("profile") 57 | credentials, err := aws.GetAwsCredentialsForProfile(profile) 58 | if err != nil { 59 | return nil, err 60 | } 61 | return aws.NewClientWithCredentials(*region, credentials) 62 | } else { 63 | return aws.NewClient(*region) 64 | } 65 | } 66 | 67 | // Retrieve Amazon SSM parameters as environment variables for a given prefix from CLI context 68 | func RetrieveEnvironmentVariablesFromContext(c *cli.Context) (*environment.Environment, error) { 69 | prefix, err := GetMandatoryFlag(c, "prefix") 70 | if err != nil { 71 | return nil, err 72 | } 73 | client, err := NewClientFromContext(c) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | parameters, err := client.WithPrefix(*prefix) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | environment, err := environment.NewEnvironmentFromDecryptedParameters(parameters) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | if c.GlobalIsSet("forwarded-profile") { 89 | environmentWithAwsCredentials, err := EnvironmentForAwsCredentialsProfile(c.GlobalString("forwarded-profile")) 90 | if err != nil { 91 | return nil, err 92 | } 93 | environment = environment.Union(environmentWithAwsCredentials) 94 | } 95 | 96 | return environment, nil 97 | } 98 | 99 | 100 | func GetCommands() []cli.Command { 101 | return []cli.Command{ 102 | { 103 | Name: "output", 104 | Aliases: []string{"o"}, 105 | Usage: "retrieve and atomically output environment variables to a file", 106 | Action: output, 107 | Flags: []cli.Flag{ 108 | cli.StringFlag{ 109 | Name: "env-file", 110 | Usage: "Output file to write environment variables to", 111 | }, 112 | }, 113 | }, 114 | { 115 | Name: "exec", 116 | Aliases: []string{"e"}, 117 | Usage: "retrieve environment variables and execute command with an updated environment", 118 | Action: execRun, 119 | }, 120 | } 121 | } -------------------------------------------------------------------------------- /commands/exec.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "syscall" 8 | "os/signal" 9 | 10 | "github.com/urfave/cli" 11 | "github.com/nlindblad/confidential/environment" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | type commandWithFlags struct { 16 | executable string 17 | arguments []string 18 | } 19 | 20 | func extractCommandWithFlags(rawArgs []string) (*commandWithFlags, error) { 21 | start := -1 22 | for i, s := range rawArgs { 23 | if s == "--" { 24 | start = i + 1 25 | break 26 | } 27 | } 28 | 29 | if start == -1 || start + 1 > len(rawArgs) { 30 | return nil, fmt.Errorf("no command provided") 31 | } 32 | 33 | return &commandWithFlags{executable: rawArgs[start], arguments: rawArgs[start + 1:]}, nil 34 | } 35 | 36 | func getEnvironmentFromContext(c *cli.Context) (*environment.Environment, error) { 37 | retrievedEnvironment, err := RetrieveEnvironmentVariablesFromContext(c) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | runtimeEnvironment, err := environment.NewEnvironmentFromRuntime(os.Environ()) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | newEnvironment := runtimeEnvironment.Union(retrievedEnvironment) 48 | 49 | return newEnvironment, nil 50 | } 51 | 52 | func execRun(c *cli.Context) error { 53 | curatedEnvironment, err := getEnvironmentFromContext(c) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | args := os.Args[1:] 59 | cmd, err := extractCommandWithFlags(args) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | newCmd := exec.Command(cmd.executable, cmd.arguments...) 65 | newCmd.Stdin = os.Stdin 66 | newCmd.Stdout = os.Stdout 67 | newCmd.Stderr = os.Stderr 68 | newCmd.Env = curatedEnvironment.AsStrings() 69 | 70 | // Forward SIGINT, SIGTERM, SIGKILL to the child command 71 | sigChan := make(chan os.Signal, 1) 72 | signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt, os.Kill) 73 | 74 | go func() { 75 | sig := <-sigChan 76 | if newCmd.Process != nil { 77 | newCmd.Process.Signal(sig) 78 | } 79 | }() 80 | 81 | var waitStatus syscall.WaitStatus 82 | if err := newCmd.Run(); err != nil { 83 | if err != nil { 84 | return errors.Wrap(err, "failed to run command") 85 | } 86 | if exitError, ok := err.(*exec.ExitError); ok { 87 | waitStatus = exitError.Sys().(syscall.WaitStatus) 88 | os.Exit(waitStatus.ExitStatus()) 89 | } 90 | } 91 | return nil 92 | } -------------------------------------------------------------------------------- /commands/exec_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "testing" 5 | "github.com/stretchr/testify/assert" 6 | ) 7 | 8 | func Test_extractCommandWithFlags(t *testing.T) { 9 | var err error 10 | 11 | cmd, err := extractCommandWithFlags([]string{"--prefix", "/test/prod", "--region", "eu-west-1", "exec", "--", "env"}) 12 | assert.Equal(t, "env", cmd.executable) 13 | assert.Equal(t, 0, len(cmd.arguments)) 14 | 15 | cmd, err = extractCommandWithFlags([]string{"--prefix", "/test/prod", "--region", "eu-west-1", "exec", "--", "echo", "\"${SECRET_KEY}\""}) 16 | assert.Equal(t, "echo", cmd.executable) 17 | assert.Equal(t, 1, len(cmd.arguments)) 18 | 19 | cmd, err = extractCommandWithFlags([]string{"--prefix", "/test/prod", "--region", "eu-west-1", "exec", "--"}) 20 | assert.NotNil(t, err) 21 | } -------------------------------------------------------------------------------- /commands/output.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/dchest/safefile" 5 | "github.com/nlindblad/confidential/environment" 6 | "github.com/urfave/cli" 7 | "fmt" 8 | ) 9 | 10 | func writeEnvironmentToFile(environment *environment.Environment, envFile *string) (error) { 11 | f, err := safefile.Create(*envFile, 0644) 12 | if err != nil { 13 | return nil 14 | } 15 | defer f.Close() 16 | 17 | for _, environmentVariable := range environment.AsStrings() { 18 | _, err := f.WriteString(fmt.Sprintf("%s\n", environmentVariable)) 19 | if err != nil { 20 | return err 21 | } 22 | } 23 | 24 | err = f.Commit() 25 | if err != nil { 26 | return err 27 | } 28 | 29 | return nil 30 | } 31 | 32 | func output(c *cli.Context) error { 33 | environment, err := RetrieveEnvironmentVariablesFromContext(c) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | envFile, err := GetMandatoryFlag(c, "env-file") 39 | if err != nil { 40 | return err 41 | } 42 | 43 | writeEnvironmentToFile(environment, envFile) 44 | 45 | return nil 46 | } -------------------------------------------------------------------------------- /environment/environment.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "fmt" 7 | "sort" 8 | 9 | "github.com/nlindblad/confidential/aws" 10 | ) 11 | 12 | // Environment variable 13 | type Variable struct { 14 | Name string 15 | Value string 16 | } 17 | 18 | // List of environment variables 19 | type Environment struct { 20 | variables map[string]string 21 | } 22 | 23 | func NewEnvironment() (*Environment) { 24 | var environment Environment 25 | environment.variables = make(map[string]string) 26 | 27 | return &environment 28 | } 29 | 30 | // Create new list of environment variables from a list of decrypted Amazon SSM parameters 31 | func NewEnvironmentFromDecryptedParameters(decryptedParameters aws.DecryptedParameters) (*Environment, error) { 32 | var environment Environment 33 | environment.variables = make(map[string]string) 34 | 35 | for _, param := range decryptedParameters { 36 | name, err := toEnvironmentVariableName(param.Name) 37 | 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | environment.Add(Variable{*name, param.Value}) 43 | } 44 | 45 | return &environment, nil 46 | } 47 | 48 | // Create a new environment from the runtime environment 49 | func NewEnvironmentFromRuntime(runtimeEnvironment []string) (*Environment, error) { 50 | var environment Environment 51 | environment.variables = make(map[string]string) 52 | 53 | for _, variable := range runtimeEnvironment { 54 | parts := strings.Split(variable, "=") 55 | if len(parts) < 2 { 56 | return nil, fmt.Errorf("invalid environment variable from runtime: %s", variable) 57 | } 58 | name := parts[0] 59 | value := parts[1] 60 | environment.Add(Variable{name, value}) 61 | } 62 | 63 | return &environment, nil 64 | } 65 | 66 | // Get the union of two sets of environment variable 67 | func (env *Environment) Union(otherEnv *Environment) (*Environment) { 68 | var newEnvironment Environment 69 | newEnvironment.variables = make(map[string]string) 70 | 71 | for k, v := range env.variables { 72 | newEnvironment.variables[k] = v 73 | } 74 | 75 | for k, v := range otherEnv.variables { 76 | newEnvironment.variables[k] = v 77 | } 78 | 79 | return &newEnvironment 80 | } 81 | 82 | // Unset a list of environment variables 83 | func (env *Environment) Unset(names []string) { 84 | for _, name := range names { 85 | delete(env.variables, name) 86 | } 87 | } 88 | 89 | // Add a new environment variable 90 | func (env *Environment) Add(environmentVariable Variable) { 91 | env.variables[environmentVariable.Name] = environmentVariable.Value 92 | } 93 | 94 | // Get a sorted list representation of the environment 95 | func (env *Environment) AsList() []Variable { 96 | var environmentList []Variable 97 | 98 | var keys []string 99 | for k := range env.variables { 100 | keys = append(keys, k) 101 | } 102 | sort.Strings(keys) 103 | 104 | for _, name := range keys { 105 | value := env.variables[name] 106 | environmentList = append(environmentList, Variable{name, value}) 107 | } 108 | 109 | return environmentList 110 | } 111 | 112 | // Get string array representation of the environment 113 | func (env *Environment) AsStrings() []string { 114 | var environmentVariables []string 115 | 116 | for _, environmentVariable := range env.AsList() { 117 | environmentVariables = append(environmentVariables, fmt.Sprintf("%s=%s", environmentVariable.Name, environmentVariable.Value)) 118 | } 119 | 120 | return environmentVariables 121 | } 122 | 123 | func toEnvironmentVariableName(name string) (*string, error) { 124 | allowedCharactersRegexp, err := regexp.Compile("[^-./_a-zA-Z0-9]+") 125 | if err != nil { 126 | return nil, err 127 | } 128 | nonAlphaNumericalCharactersRegexp, err := regexp.Compile("[^a-zA-Z0-9]+") 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | nameWithOnlyAllowedCharacters := allowedCharactersRegexp.ReplaceAllString(name, "") 134 | nameWithOutSpecialCharacters := nonAlphaNumericalCharactersRegexp.ReplaceAllString(nameWithOnlyAllowedCharacters, "_") 135 | nameUpperCased := strings.ToUpper(nameWithOutSpecialCharacters) 136 | 137 | return &nameUpperCased, nil 138 | } 139 | -------------------------------------------------------------------------------- /environment/environment_test.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | import ( 4 | "testing" 5 | "github.com/stretchr/testify/assert" 6 | ) 7 | 8 | func Test_toEnvironmentVariableName(t *testing.T) { 9 | environmentVariableName, _ := toEnvironmentVariableName("secret_key") 10 | assert.Equal(t, "SECRET_KEY", *environmentVariableName) 11 | 12 | environmentVariableName, _ = toEnvironmentVariableName("secret/key") 13 | assert.Equal(t, "SECRET_KEY", *environmentVariableName) 14 | 15 | environmentVariableName, _ = toEnvironmentVariableName("secret key") 16 | assert.Equal(t, "SECRETKEY", *environmentVariableName) 17 | 18 | environmentVariableName, _ = toEnvironmentVariableName(".secret-key") 19 | assert.Equal(t, "_SECRET_KEY", *environmentVariableName) 20 | 21 | environmentVariableName, _ = toEnvironmentVariableName(".secret#-@key") 22 | assert.Equal(t, "_SECRET_KEY", *environmentVariableName) 23 | } 24 | 25 | func Test_Union_NoOverlap(t *testing.T) { 26 | expectedEnvironment := []string{ 27 | "A=1", 28 | "B=2", 29 | "C=3", 30 | "D=4", 31 | "E=5", 32 | "F=6", 33 | } 34 | 35 | firstEnvironment := NewEnvironment() 36 | firstEnvironment.Add(Variable{"A", "1"}) 37 | firstEnvironment.Add(Variable{"B", "2"}) 38 | firstEnvironment.Add(Variable{"D", "4"}) 39 | secondEnvironment := NewEnvironment() 40 | secondEnvironment.Add(Variable{"C", "3"}) 41 | secondEnvironment.Add(Variable{"E", "5"}) 42 | secondEnvironment.Add(Variable{"F", "6"}) 43 | 44 | firstUnion := firstEnvironment.Union(secondEnvironment) 45 | 46 | assert.Equal(t, expectedEnvironment, firstUnion.AsStrings()) 47 | } 48 | 49 | func Test_Union_HasOverlap(t *testing.T) { 50 | expectedEnvironment := []string{ 51 | "A=1", 52 | "B=12", 53 | "C=13", 54 | "D=4", 55 | } 56 | 57 | firstEnvironment := NewEnvironment() 58 | firstEnvironment.Add(Variable{"A", "1"}) 59 | firstEnvironment.Add(Variable{"B", "2"}) 60 | firstEnvironment.Add(Variable{"C", "3"}) 61 | secondEnvironment := NewEnvironment() 62 | secondEnvironment.Add(Variable{"B", "12"}) 63 | secondEnvironment.Add(Variable{"C", "13"}) 64 | secondEnvironment.Add(Variable{"D", "4"}) 65 | 66 | firstUnion := firstEnvironment.Union(secondEnvironment) 67 | 68 | assert.Equal(t, expectedEnvironment, firstUnion.AsStrings()) 69 | } -------------------------------------------------------------------------------- /examples/cloudformation/example-3-cloudformation.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: "2010-09-09" 3 | Description: "Simple autoscaling EC2 service using secrets management" 4 | 5 | Parameters: 6 | KmsKeyId: 7 | Type: "String" 8 | EnvironmentName: 9 | Type: "String" 10 | InstanceType: 11 | Type: "String" 12 | Default: "t2.nano" 13 | KeyPairName: 14 | Type: "AWS::EC2::KeyPair::KeyName" 15 | ProjectName: 16 | Type: "String" 17 | 18 | Mappings: 19 | RegionMap: 20 | us-east-2: 21 | "64": ami-15e9c770 22 | us-east-1: 23 | "64": ami-55ef662f 24 | us-west-1: 25 | "64": ami-a51f27c5 26 | us-west-2: 27 | "64": ami-bf4193c7 28 | ca-central-1: 29 | "64": ami-d29e25b6 30 | ap-south-1: 31 | "64": ami-d5c18eba 32 | ap-northeast-2: 33 | "64": ami-1196317f 34 | ap-southeast-1: 35 | "64": ami-c63d6aa5 36 | ap-southeast-2: 37 | "64": ami-ff4ea59d 38 | ap-northeast-1: 39 | "64": ami-da9e2cbc 40 | eu-central-1: 41 | "64": ami-bf2ba8d0 42 | eu-west-1: 43 | "64": ami-1a962263 44 | eu-west-2: 45 | "64": ami-e7d6c983 46 | sa-east-1: 47 | "64": ami-286f2a44 48 | 49 | Resources: 50 | InstanceIamRole: 51 | Type: "AWS::IAM::Role" 52 | Properties: 53 | AssumeRolePolicyDocument: 54 | Statement: 55 | - Action: 56 | - "sts:AssumeRole" 57 | Effect: "Allow" 58 | Principal: 59 | Service: 60 | - "ec2.amazonaws.com" 61 | Policies: 62 | - PolicyName: "secrets-management" 63 | PolicyDocument: 64 | Version: "2012-10-17" 65 | Id: "AllowAccessToParameters" 66 | Statement: 67 | - Sid: "AllowAccessToGetParametersByPath" 68 | Effect: "Allow" 69 | Action: "ssm:GetParametersByPath" 70 | Resource: 71 | - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${ProjectName}/${EnvironmentName}" 72 | - Sid: "AllowAccessToDecryptParameters" 73 | Effect: "Allow" 74 | Action: "kms:Decrypt" 75 | Resource: 76 | - !Sub "arn:aws:kms:${AWS::Region}:*:key/${KmsKeyId}" 77 | EC2InstanceProfile: 78 | Type: "AWS::IAM::InstanceProfile" 79 | Properties: 80 | Roles: 81 | - !Ref InstanceIamRole 82 | SSHSecurityGroup: 83 | Type: AWS::EC2::SecurityGroup 84 | Properties: 85 | GroupDescription: Enable SSH access via port 22 86 | SecurityGroupIngress: 87 | - CidrIp: 0.0.0.0/0 88 | FromPort: 22 89 | IpProtocol: tcp 90 | ToPort: 22 91 | ECInstance: 92 | Type: AWS::EC2::Instance 93 | Properties: 94 | KeyName: !Ref KeyPairName 95 | IamInstanceProfile: !Ref EC2InstanceProfile 96 | SecurityGroups: 97 | - !Ref SSHSecurityGroup 98 | UserData: 99 | Fn::Base64: !Sub | 100 | #!/bin/bash -xe 101 | exit 0 102 | InstanceType: !Ref InstanceType 103 | AvailabilityZone: 104 | Fn::Select: 105 | - 0 106 | - Fn::GetAZs: !Ref "AWS::Region" 107 | ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", 64] 108 | Tags: 109 | - 110 | Key: Name 111 | Value: "SSM test instance" -------------------------------------------------------------------------------- /examples/cloudformation/example-4-cloudformation.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: IAM role for accessing SSM parameters 3 | 4 | Parameters: 5 | Name: 6 | Description: Name of user 7 | Type: String 8 | KmsKeyId: 9 | Type: "String" 10 | EnvironmentName: 11 | Type: "String" 12 | ProjectName: 13 | Type: "String" 14 | 15 | Resources: 16 | SsmParameterUser: 17 | Type: "AWS::IAM::User" 18 | Properties: 19 | Policies: 20 | - PolicyName: "secrets-management" 21 | PolicyDocument: 22 | Version: "2012-10-17" 23 | Id: "AllowAccessToParameters" 24 | Statement: 25 | - Sid: "AllowAccessToGetParametersByPath" 26 | Effect: "Allow" 27 | Action: "ssm:GetParametersByPath" 28 | Resource: 29 | - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${ProjectName}/${EnvironmentName}" 30 | - Sid: "AllowAccessToDecryptParameters" 31 | Effect: "Allow" 32 | Action: "kms:Decrypt" 33 | Resource: 34 | - !Sub "arn:aws:kms:${AWS::Region}:*:key/${KmsKeyId}" 35 | UserName: !Join [ "-", [ !Ref Name, "ssm" ] ] 36 | 37 | SsmParameterUserKey: 38 | DependsOn: SsmParameterUser 39 | Type: "AWS::IAM::AccessKey" 40 | Properties: 41 | Status: Active 42 | UserName: !Ref SsmParameterUser 43 | 44 | Outputs: 45 | AccessKey: 46 | Value: 47 | Ref: SsmParameterUserKey 48 | SecretKey: 49 | Value: !GetAtt SsmParameterUserKey.SecretAccessKey 50 | -------------------------------------------------------------------------------- /examples/cloudformation/example-5-cloudformation.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: IAM role for updating SSM parameters 3 | 4 | Parameters: 5 | Name: 6 | Description: Name of user 7 | Type: String 8 | KmsKeyId: 9 | Type: "String" 10 | EnvironmentName: 11 | Type: "String" 12 | ProjectName: 13 | Type: "String" 14 | 15 | Resources: 16 | SsmParameterUser: 17 | Type: "AWS::IAM::User" 18 | Properties: 19 | Policies: 20 | - PolicyName: "secrets-management" 21 | PolicyDocument: 22 | Version: "2012-10-17" 23 | Id: "AllowAccessToParameters" 24 | Statement: 25 | - Sid: "AllowAccessToPutParameter" 26 | Effect: "Allow" 27 | Action: "ssm:PutParameter" 28 | Resource: 29 | - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${ProjectName}/${EnvironmentName}/*" 30 | - Sid: "AllowAccessToEncryptParameters" 31 | Effect: "Allow" 32 | Action: "kms:Encrypt" 33 | Resource: 34 | - !Sub "arn:aws:kms:${AWS::Region}:*:key/${KmsKeyId}" 35 | UserName: !Join [ "-", [ !Ref Name, "ssm" ] ] 36 | 37 | SsmParameterUserKey: 38 | DependsOn: SsmParameterUser 39 | Type: "AWS::IAM::AccessKey" 40 | Properties: 41 | Status: Active 42 | UserName: !Ref SsmParameterUser 43 | 44 | Outputs: 45 | AccessKey: 46 | Value: 47 | Ref: SsmParameterUserKey 48 | SecretKey: 49 | Value: !GetAtt SsmParameterUserKey.SecretAccessKey 50 | --------------------------------------------------------------------------------