├── .github └── workflows │ ├── build.yml │ ├── gochecks.yml │ ├── jira.yaml │ └── test.yml ├── .gitignore ├── .go-version ├── .release ├── ci.hcl ├── release-metadata.hcl └── security-scan.hcl ├── CHANGELOG.md ├── CODEOWNERS ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── internal ├── config │ ├── auth.go │ ├── cache.go │ ├── cache_test.go │ ├── info.go │ ├── secret.go │ ├── secret_test.go │ └── useragent.go ├── extension │ └── client.go ├── proxy │ ├── cache.go │ ├── cache_test.go │ ├── proxy.go │ └── proxy_test.go ├── runmode │ └── runmode.go ├── ststest │ └── sts.go └── vault │ ├── client.go │ └── client_test.go ├── main.go ├── quick-start ├── README.md ├── build-container.sh ├── build.sh ├── demo-function │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── bin │ │ └── bootstrap │ ├── go.mod │ ├── go.sum │ └── main.go └── terraform │ ├── .gitignore │ ├── aws.tf │ ├── iam.tf │ ├── kms.tf │ ├── lambda.tf │ ├── outputs.tf │ ├── rds.tf │ ├── security-groups.tf │ ├── ssh-key.tf │ ├── templates │ └── userdata-vault-server.tpl │ ├── variables.tf │ ├── vault-server.tf │ └── versions.tf └── test ├── README.md ├── api ├── Dockerfile ├── README.md └── main.go ├── docker-compose.yaml └── lambda ├── Dockerfile └── runtime.sh /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | workflow_dispatch: 6 | inputs: 7 | version: 8 | description: "Version to build, e.g. 0.1.0" 9 | type: string 10 | required: false 11 | 12 | env: 13 | PKG_NAME: "vault-lambda-extension" 14 | 15 | jobs: 16 | get-product-version: 17 | runs-on: ubuntu-latest 18 | outputs: 19 | product-version: ${{ steps.get-product-version.outputs.product-version }} 20 | steps: 21 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 22 | - name: get product version 23 | id: get-product-version 24 | run: | 25 | VERSION="${{ github.event.inputs.version || '0.0.0-dev' }}" 26 | echo "Using version ${VERSION}" 27 | echo "product-version=${VERSION}" >> "$GITHUB_OUTPUT" 28 | 29 | generate-metadata-file: 30 | needs: get-product-version 31 | runs-on: ubuntu-latest 32 | outputs: 33 | filepath: ${{ steps.generate-metadata-file.outputs.filepath }} 34 | steps: 35 | - name: "Checkout directory" 36 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 37 | - name: Generate metadata file 38 | id: generate-metadata-file 39 | uses: hashicorp/actions-generate-metadata@v1 40 | with: 41 | version: ${{ needs.get-product-version.outputs.product-version }} 42 | product: ${{ env.PKG_NAME }} 43 | repositoryOwner: "hashicorp" 44 | - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 45 | with: 46 | name: metadata.json 47 | path: ${{ steps.generate-metadata-file.outputs.filepath }} 48 | 49 | build: 50 | needs: 51 | - get-product-version 52 | runs-on: ubuntu-latest 53 | strategy: 54 | matrix: 55 | goos: [linux] 56 | goarch: ["arm64", "amd64"] 57 | fail-fast: true 58 | 59 | name: Go ${{ matrix.goos }} ${{ matrix.goarch }} build 60 | 61 | steps: 62 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 63 | 64 | - name: Setup go 65 | uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 66 | with: 67 | go-version-file: .go-version 68 | 69 | - name: Build 70 | id: build-binary 71 | run: | 72 | ZIP_FILE="${{ env.PKG_NAME }}_${{ needs.get-product-version.outputs.product-version }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip" 73 | 74 | make zip 75 | mv pkg/${{ env.PKG_NAME }}.zip pkg/"$ZIP_FILE" 76 | 77 | echo "name=${ZIP_FILE}" >> "$GITHUB_OUTPUT" 78 | echo "path=pkg/${ZIP_FILE}" >> "$GITHUB_OUTPUT" 79 | 80 | - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 81 | with: 82 | name: ${{ steps.build-binary.outputs.name }} 83 | path: ${{ steps.build-binary.outputs.path }} 84 | -------------------------------------------------------------------------------- /.github/workflows/gochecks.yml: -------------------------------------------------------------------------------- 1 | name: Go checks 2 | on: 3 | push: 4 | jobs: 5 | go-checks: 6 | # using `main` as the ref will keep your workflow up-to-date 7 | uses: hashicorp/vault-workflows-common/.github/workflows/go-checks.yaml@main -------------------------------------------------------------------------------- /.github/workflows/jira.yaml: -------------------------------------------------------------------------------- 1 | name: Jira Sync 2 | on: 3 | issues: 4 | types: [opened, closed, deleted, reopened] 5 | pull_request_target: 6 | types: [opened, closed, reopened] 7 | issue_comment: # Also triggers when commenting on a PR from the conversation view 8 | types: [created] 9 | jobs: 10 | sync: 11 | uses: hashicorp/vault-workflows-common/.github/workflows/jira.yaml@main 12 | secrets: 13 | JIRA_SYNC_BASE_URL: ${{ secrets.JIRA_SYNC_BASE_URL }} 14 | JIRA_SYNC_USER_EMAIL: ${{ secrets.JIRA_SYNC_USER_EMAIL }} 15 | JIRA_SYNC_API_TOKEN: ${{ secrets.JIRA_SYNC_API_TOKEN }} 16 | with: 17 | teams-array: '["ecosystem", "applications"]' 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | # Run this workflow on pushes and manually 4 | on: [push, workflow_dispatch] 5 | 6 | jobs: 7 | get-go-version: 8 | name: "Determine Go toolchain version" 9 | runs-on: ubuntu-latest 10 | outputs: 11 | go-version: ${{ steps.get-go-version.outputs.go-version }} 12 | steps: 13 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 14 | - name: Determine Go version 15 | id: get-go-version 16 | run: | 17 | echo "Building with Go $(cat .go-version)" 18 | echo "::set-output name=go-version::$(cat .go-version)" 19 | 20 | golangci: 21 | name: lint 22 | needs: 23 | - get-go-version 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 27 | with: 28 | go-version: ${{ needs.get-go-version.outputs.go-version }} 29 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 30 | - name: golangci-lint 31 | uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0 32 | with: 33 | args: | 34 | -v --concurrency 2 \ 35 | --disable-all \ 36 | --timeout 10m \ 37 | --enable gofmt \ 38 | --enable gosimple \ 39 | --enable govet 40 | 41 | run-tests: 42 | # using `main` as the ref will keep your workflow up-to-date 43 | uses: hashicorp/vault-workflows-common/.github/workflows/tests.yaml@main 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | pkg 3 | .idea 4 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.24.3 2 | -------------------------------------------------------------------------------- /.release/ci.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | schema = "1" 5 | 6 | project "vault-lambda-extension" { 7 | team = "vault" 8 | slack { 9 | notification_channel = "C03RXFX5M4L" // #feed-vault-releases 10 | } 11 | github { 12 | organization = "hashicorp" 13 | repository = "vault-lambda-extension" 14 | release_branches = ["main"] 15 | } 16 | } 17 | 18 | event "merge" { 19 | // "entrypoint" to use if build is not run automatically 20 | // i.e. send "merge" complete signal to orchestrator to trigger build 21 | } 22 | 23 | event "build" { 24 | depends = ["merge"] 25 | action "build" { 26 | organization = "hashicorp" 27 | repository = "vault-lambda-extension" 28 | workflow = "build" 29 | } 30 | } 31 | 32 | event "prepare" { 33 | depends = ["build"] 34 | action "prepare" { 35 | organization = "hashicorp" 36 | repository = "crt-workflows-common" 37 | workflow = "prepare" 38 | depends = ["build"] 39 | } 40 | 41 | notification { 42 | on = "fail" 43 | } 44 | } 45 | 46 | ## These are promotion and post-publish events 47 | ## they should be added to the end of the file after the verify event stanza. 48 | 49 | event "trigger-staging" { 50 | // This event is dispatched by the bob trigger-promotion command 51 | // and is required - do not delete. 52 | } 53 | 54 | event "promote-staging" { 55 | depends = ["trigger-staging"] 56 | action "promote-staging" { 57 | organization = "hashicorp" 58 | repository = "crt-workflows-common" 59 | workflow = "promote-staging" 60 | config = "release-metadata.hcl" 61 | } 62 | 63 | notification { 64 | on = "always" 65 | } 66 | } 67 | 68 | event "promote-layer-staging" { 69 | depends = ["promote-staging"] 70 | action "promote-layer-staging" { 71 | organization = "hashicorp" 72 | repository = "vault-lambda-extension-release" 73 | workflow = "promote-layer-staging" 74 | } 75 | } 76 | 77 | event "trigger-production" { 78 | // This event is dispatched by the bob trigger-promotion command 79 | // and is required - do not delete. 80 | } 81 | 82 | event "promote-production" { 83 | depends = ["trigger-production"] 84 | action "promote-production" { 85 | organization = "hashicorp" 86 | repository = "crt-workflows-common" 87 | workflow = "promote-production" 88 | } 89 | 90 | notification { 91 | on = "always" 92 | } 93 | } 94 | 95 | event "promote-layer-production" { 96 | depends = ["promote-production"] 97 | action "promote-layer-production" { 98 | organization = "hashicorp" 99 | repository = "vault-lambda-extension-release" 100 | workflow = "promote-layer-production" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /.release/release-metadata.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | url_license = "https://github.com/hashicorp/vault-lambda-extension/blob/main/LICENSE" 5 | url_project_website = "https://github.com/hashicorp/vault-lambda-extension#readme" 6 | url_source_repository = "https://github.com/hashicorp/vault-lambda-extension" -------------------------------------------------------------------------------- /.release/security-scan.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | binary { 5 | secrets = true 6 | go_modules = true 7 | osv = true 8 | oss_index = false 9 | nvd = false 10 | 11 | triage { 12 | suppress { 13 | vulnerabilites = [ 14 | "GHSA-f5pg-7wfw-84q9", # AWS S3 Crypto SDK vuln https://osv.dev/vulnerability/GO-2022-0646 15 | "GO-2022-0646", # alias 16 | "GHSA-7f33-f4f5-xwgw", # AWS S3 Crypto SDK vuln https://osv.dev/vulnerability/GO-2022-0635 17 | "GO-2022-0635" #alias 18 | ] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | ## 0.13.0 (Jun 5, 2025) 4 | 5 | LAYERS: 6 | ``` 7 | arn:aws:lambda::634166935893:layer:vault-lambda-extension:22 8 | arn:aws:lambda::634166935893:layer:vault-lambda-extension-arm64:10 9 | ``` 10 | 11 | IMPROVEMENTS: 12 | 13 | * Bumped versions for the following dependencies: 14 | * golang.org/x/net v0.38.0 15 | * golang.org/x/crypto v0.36.0 16 | * golang.org/x/sys v0.31.0 17 | * golang.org/x/text v0.23.0 18 | * github.com/go-jose/go-jose/v4 v4.0.5 19 | 20 | ## 0.12.0 (Feb 20, 2025) 21 | 22 | LAYERS: 23 | ``` 24 | arn:aws:lambda::634166935893:layer:vault-lambda-extension:21 25 | arn:aws:lambda::634166935893:layer:vault-lambda-extension-arm64:9 26 | ``` 27 | 28 | IMPROVEMENTS: 29 | 30 | * Building with Go 1.23.6 31 | * Bumped versions for the following dependencies: 32 | * github.com/aws/aws-sdk-go v1.55.6 33 | * github.com/hashicorp/vault/api v1.15.0 34 | * github.com/hashicorp/vault/sdk v1.15.0 35 | * github.com/stretchr/testify v1.10.0 36 | * golang.org/x/crypto v0.33.0 37 | * golang.org/x/net v0.35.0 38 | * golang.org/x/sys v0.30.0 39 | * golang.org/x/text v0.22.0 40 | * github.com/cenkalti/backoff/v4 v4.3.0 41 | * github.com/fatih/color v1.17.0 42 | * github.com/go-jose/go-jose/v4 v4.0.4 43 | * github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9 44 | * github.com/hashicorp/go-sockaddr v1.0.6 45 | * golang.org/x/time v0.9.0 46 | * golang.org/x/text v0.7.0 47 | 48 | ## 0.11.0 (Sept 6, 2024) 49 | 50 | LAYERS: 51 | ``` 52 | arn:aws:lambda::634166935893:layer:vault-lambda-extension:20 53 | arn:aws:lambda::634166935893:layer:vault-lambda-extension-arm64:8 54 | ``` 55 | 56 | FEATURES: 57 | 58 | * Add optional header `X-Vault-Token-Options` to revoke token when set to `revoke` (#149) 59 | 60 | IMPROVEMENTS: 61 | 62 | * update go-retryablehttp (#144) 63 | * Exclude distributed tracing headers from cache key (#145) 64 | 65 | BUGS: 66 | 67 | * Strip Monotonic Clock to prevent Lambda suspension from miscalculating TTL. Fixes #113. (#150) 68 | 69 | ## 0.10.3 (May 1, 2024) 70 | LAYERS: 71 | ``` 72 | arn:aws:lambda::634166935893:layer:vault-lambda-extension:19 73 | arn:aws:lambda::634166935893:layer:vault-lambda-extension-arm64:7 74 | ``` 75 | 76 | Dependency Updates: 77 | * Bump dependencies (https://github.com/hashicorp/vault-lambda-extension/pull/135): 78 | * golang.org/x/crypto to v0.22.0 79 | * golang.org/x/net to v0.24.0 80 | * golang.org/x/sys to v0.19.0 81 | * Bump dependencies (https://github.com/hashicorp/vault-lambda-extension/pull/134): 82 | * go to 1.22.2 83 | * vault api to v1.12.2 84 | 85 | ## 0.10.2 (February 6, 2024) 86 | 87 | LAYERS: 88 | ``` 89 | arn:aws:lambda::634166935893:layer:vault-lambda-extension:18 90 | arn:aws:lambda::634166935893:layer:vault-lambda-extension-arm64:6 91 | ``` 92 | 93 | IMPROVEMENTS: 94 | * Bumped versions for the following dependencies with security vulnerabilities: 95 | * golang.org/x/crypto v0.18.0 96 | * golang.org/x/net v0.20.0 97 | * golang.org/x/sys v0.16.0 98 | * golang.org/x/text v0.14.0 99 | * Bumped dependencies: 100 | * github.com/aws/aws-sdk-go v1.50.12 101 | * github.com/stretchr/testify v1.8.4 102 | * github.com/hashicorp/vault/api v1.11.0 103 | * github.com/hashicorp/vault/sdk v0.10.2 104 | 105 | ## 0.10.1 (July 10, 2023) 106 | 107 | LAYERS: 108 | ``` 109 | arn:aws:lambda::634166935893:layer:vault-lambda-extension:17 110 | arn:aws:lambda::634166935893:layer:vault-lambda-extension-arm64:5 111 | ``` 112 | 113 | IMPROVEMENTS: 114 | * quick-start: Update Postgres version to 14.7 115 | * Add debug logs during initialization step 116 | 117 | ## 0.10.0 (March 30, 2023) 118 | 119 | LAYERS: 120 | ``` 121 | arn:aws:lambda::634166935893:layer:vault-lambda-extension:16 122 | arn:aws:lambda::634166935893:layer:vault-lambda-extension-arm64:4 123 | ``` 124 | 125 | IMPROVEMENTS: 126 | * Requests from the extension to Vault now set the User-Agent field accordingly. 127 | * Introduced a `VAULT_RUN_MODE` environment variable to allow user to run in proxy mode, file mode, or both. 128 | * The default value is 'default', which runs in *both* proxy and file mode. 129 | * Vault Lambda Extension version dynamically injected at build time. 130 | 131 | ## 0.9.0 (February 23, 2023) 132 | 133 | IMPROVEMENTS: 134 | * Building with Go 1.19.6 135 | * Bumped versions for the following dependencies with security vulnerabilities: 136 | * github.com/hashicorp/vault/api v1.9.0 137 | * github.com/hashicorp/vault/sdk v0.8.1 138 | * golang.org/x/net v0.7.0 139 | * golang.org/x/sys v0.5.0 140 | * golang.org/x/text v0.7.0 141 | 142 | ## 0.8.0 (August 22, 2022) 143 | 144 | FEATURES: 145 | 146 | * `VAULT_ASSUMED_ROLE_ARN` can be used to specify a role for your lambda function to assume. [[GH-69](https://github.com/hashicorp/vault-lambda-extension/pull/69)] 147 | 148 | IMPROVEMENTS: 149 | 150 | * Bumped versions for the following dependencies with security vulnerabilities: 151 | * golang.org/x/crypto to v0.0.0-20220817201139-bc19a97f63c8 152 | * golang.org/x/net to v0.0.0-20220812174116-3211cb980234 153 | * golang.org/x/sys to v0.0.0-20220818161305-2296e01440c6 154 | * golang.org/x/text to v0.3.7 155 | 156 | ## 0.7.0 (April 26, 2022) 157 | 158 | CHANGES: 159 | 160 | * Static function code can now reliably read secrets written to disk, because extension registration now occurs after writing files. [[GH-61](https://github.com/hashicorp/vault-lambda-extension/pull/61)] 161 | * arm64 architecture now supported [[GH-67](https://github.com/hashicorp/vault-lambda-extension/pull/67)] 162 | 163 | ## 0.6.0 (March 14, 2022) 164 | 165 | CHANGES: 166 | 167 | * Logging is now levelled and less chatty by default. Level can be controlled by VAULT_LOG_LEVEL environment variable. [[GH-63](https://github.com/hashicorp/vault-lambda-extension/pull/63)] 168 | 169 | FEATURES: 170 | 171 | * Add caching support in the local proxy server [[GH-58](https://github.com/hashicorp/vault-lambda-extension/pull/58)) 172 | 173 | IMPROVEMENTS: 174 | 175 | * Leading and trailing whitespace is trimmed from environment variable values on reading. [[GH-63](https://github.com/hashicorp/vault-lambda-extension/pull/63)] 176 | 177 | ## 0.5.0 (August 24, 2021) 178 | 179 | FEATURES: 180 | 181 | * Use client-controlled consistency when writing secrets to disk to reliably support performance standbys/replicas. [[GH-47](https://github.com/hashicorp/vault-lambda-extension/pull/47)] 182 | 183 | ## 0.4.0 (July 1, 2021) 184 | 185 | FEATURES: 186 | 187 | * Add `VAULT_STS_ENDPOINT_REGION` environment variable to make STS regional endpoint used for auth configurable separately from the region Lambda is deployed in. [[GH-30](https://github.com/hashicorp/vault-lambda-extension/pull/30)] 188 | * Add `VLE_VAULT_ADDR` environment variable to configure Vault address to connect to. Allows clients of the proxy to consume the standard `VAULT_ADDR`. [[GH-41](https://github.com/hashicorp/vault-lambda-extension/pull/41)] 189 | 190 | DOCUMENTATION: 191 | 192 | * Added documentation for deploying the extension into `Image` format Lambdas [[GH-34](https://github.com/hashicorp/vault-lambda-extension/pull/34)] 193 | * Added documentation on performance impact of extension [[GH-35](https://github.com/hashicorp/vault-lambda-extension/pull/35)] 194 | * Added documentation on uploading the extension into different accounts and regions [[GH-37](https://github.com/hashicorp/vault-lambda-extension/pull/37)] 195 | 196 | ## v0.3.0 (March 23rd, 2021) 197 | 198 | FEATURES: 199 | 200 | * Proxy server mode: The extension now starts a Vault API proxy at 201 | `http://127.0.0.1:8200` [[GH-27](https://github.com/hashicorp/vault-lambda-extension/pull/27)] 202 | * **Breaking change:** The extension no longer writes its own Vault auth token 203 | to disk. Writing pre-configured secrets to disk remains unchanged. 204 | 205 | ## v0.2.0 (January 20th, 2021) 206 | 207 | IMPROVEMENTS: 208 | 209 | * Add Vault IAM Server ID login header if set. [[GH-21](https://github.com/hashicorp/vault-lambda-extension/pull/21)] 210 | * quick-start: Make db_instance_type configurable. 211 | 212 | ## v0.1.0 (October 8th, 2020) 213 | 214 | Features: 215 | 216 | * Initial release 217 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. Being an owner 2 | # means those groups or individuals will be added as reviewers to PRs affecting 3 | # those areas of the code. 4 | # 5 | # More on CODEOWNERS files: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 6 | 7 | * @hashicorp/vault-ecosystem 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 HashiCorp, Inc. 2 | 3 | Mozilla Public License Version 2.0 4 | ================================== 5 | 6 | 1. Definitions 7 | -------------- 8 | 9 | 1.1. "Contributor" 10 | means each individual or legal entity that creates, contributes to 11 | the creation of, or owns Covered Software. 12 | 13 | 1.2. "Contributor Version" 14 | means the combination of the Contributions of others (if any) used 15 | by a Contributor and that particular Contributor's Contribution. 16 | 17 | 1.3. "Contribution" 18 | means Covered Software of a particular Contributor. 19 | 20 | 1.4. "Covered Software" 21 | means Source Code Form to which the initial Contributor has attached 22 | the notice in Exhibit A, the Executable Form of such Source Code 23 | Form, and Modifications of such Source Code Form, in each case 24 | including portions thereof. 25 | 26 | 1.5. "Incompatible With Secondary Licenses" 27 | means 28 | 29 | (a) that the initial Contributor has attached the notice described 30 | in Exhibit B to the Covered Software; or 31 | 32 | (b) that the Covered Software was made available under the terms of 33 | version 1.1 or earlier of the License, but not also under the 34 | terms of a Secondary License. 35 | 36 | 1.6. "Executable Form" 37 | means any form of the work other than Source Code Form. 38 | 39 | 1.7. "Larger Work" 40 | means a work that combines Covered Software with other material, in 41 | a separate file or files, that is not Covered Software. 42 | 43 | 1.8. "License" 44 | means this document. 45 | 46 | 1.9. "Licensable" 47 | means having the right to grant, to the maximum extent possible, 48 | whether at the time of the initial grant or subsequently, any and 49 | all of the rights conveyed by this License. 50 | 51 | 1.10. "Modifications" 52 | means any of the following: 53 | 54 | (a) any file in Source Code Form that results from an addition to, 55 | deletion from, or modification of the contents of Covered 56 | Software; or 57 | 58 | (b) any new file in Source Code Form that contains any Covered 59 | Software. 60 | 61 | 1.11. "Patent Claims" of a Contributor 62 | means any patent claim(s), including without limitation, method, 63 | process, and apparatus claims, in any patent Licensable by such 64 | Contributor that would be infringed, but for the grant of the 65 | License, by the making, using, selling, offering for sale, having 66 | made, import, or transfer of either its Contributions or its 67 | Contributor Version. 68 | 69 | 1.12. "Secondary License" 70 | means either the GNU General Public License, Version 2.0, the GNU 71 | Lesser General Public License, Version 2.1, the GNU Affero General 72 | Public License, Version 3.0, or any later versions of those 73 | licenses. 74 | 75 | 1.13. "Source Code Form" 76 | means the form of the work preferred for making modifications. 77 | 78 | 1.14. "You" (or "Your") 79 | means an individual or a legal entity exercising rights under this 80 | License. For legal entities, "You" includes any entity that 81 | controls, is controlled by, or is under common control with You. For 82 | purposes of this definition, "control" means (a) the power, direct 83 | or indirect, to cause the direction or management of such entity, 84 | whether by contract or otherwise, or (b) ownership of more than 85 | fifty percent (50%) of the outstanding shares or beneficial 86 | ownership of such entity. 87 | 88 | 2. License Grants and Conditions 89 | -------------------------------- 90 | 91 | 2.1. Grants 92 | 93 | Each Contributor hereby grants You a world-wide, royalty-free, 94 | non-exclusive license: 95 | 96 | (a) under intellectual property rights (other than patent or trademark) 97 | Licensable by such Contributor to use, reproduce, make available, 98 | modify, display, perform, distribute, and otherwise exploit its 99 | Contributions, either on an unmodified basis, with Modifications, or 100 | as part of a Larger Work; and 101 | 102 | (b) under Patent Claims of such Contributor to make, use, sell, offer 103 | for sale, have made, import, and otherwise transfer either its 104 | Contributions or its Contributor Version. 105 | 106 | 2.2. Effective Date 107 | 108 | The licenses granted in Section 2.1 with respect to any Contribution 109 | become effective for each Contribution on the date the Contributor first 110 | distributes such Contribution. 111 | 112 | 2.3. Limitations on Grant Scope 113 | 114 | The licenses granted in this Section 2 are the only rights granted under 115 | this License. No additional rights or licenses will be implied from the 116 | distribution or licensing of Covered Software under this License. 117 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 118 | Contributor: 119 | 120 | (a) for any code that a Contributor has removed from Covered Software; 121 | or 122 | 123 | (b) for infringements caused by: (i) Your and any other third party's 124 | modifications of Covered Software, or (ii) the combination of its 125 | Contributions with other software (except as part of its Contributor 126 | Version); or 127 | 128 | (c) under Patent Claims infringed by Covered Software in the absence of 129 | its Contributions. 130 | 131 | This License does not grant any rights in the trademarks, service marks, 132 | or logos of any Contributor (except as may be necessary to comply with 133 | the notice requirements in Section 3.4). 134 | 135 | 2.4. Subsequent Licenses 136 | 137 | No Contributor makes additional grants as a result of Your choice to 138 | distribute the Covered Software under a subsequent version of this 139 | License (see Section 10.2) or under the terms of a Secondary License (if 140 | permitted under the terms of Section 3.3). 141 | 142 | 2.5. Representation 143 | 144 | Each Contributor represents that the Contributor believes its 145 | Contributions are its original creation(s) or it has sufficient rights 146 | to grant the rights to its Contributions conveyed by this License. 147 | 148 | 2.6. Fair Use 149 | 150 | This License is not intended to limit any rights You have under 151 | applicable copyright doctrines of fair use, fair dealing, or other 152 | equivalents. 153 | 154 | 2.7. Conditions 155 | 156 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 157 | in Section 2.1. 158 | 159 | 3. Responsibilities 160 | ------------------- 161 | 162 | 3.1. Distribution of Source Form 163 | 164 | All distribution of Covered Software in Source Code Form, including any 165 | Modifications that You create or to which You contribute, must be under 166 | the terms of this License. You must inform recipients that the Source 167 | Code Form of the Covered Software is governed by the terms of this 168 | License, and how they can obtain a copy of this License. You may not 169 | attempt to alter or restrict the recipients' rights in the Source Code 170 | Form. 171 | 172 | 3.2. Distribution of Executable Form 173 | 174 | If You distribute Covered Software in Executable Form then: 175 | 176 | (a) such Covered Software must also be made available in Source Code 177 | Form, as described in Section 3.1, and You must inform recipients of 178 | the Executable Form how they can obtain a copy of such Source Code 179 | Form by reasonable means in a timely manner, at a charge no more 180 | than the cost of distribution to the recipient; and 181 | 182 | (b) You may distribute such Executable Form under the terms of this 183 | License, or sublicense it under different terms, provided that the 184 | license for the Executable Form does not attempt to limit or alter 185 | the recipients' rights in the Source Code Form under this License. 186 | 187 | 3.3. Distribution of a Larger Work 188 | 189 | You may create and distribute a Larger Work under terms of Your choice, 190 | provided that You also comply with the requirements of this License for 191 | the Covered Software. If the Larger Work is a combination of Covered 192 | Software with a work governed by one or more Secondary Licenses, and the 193 | Covered Software is not Incompatible With Secondary Licenses, this 194 | License permits You to additionally distribute such Covered Software 195 | under the terms of such Secondary License(s), so that the recipient of 196 | the Larger Work may, at their option, further distribute the Covered 197 | Software under the terms of either this License or such Secondary 198 | License(s). 199 | 200 | 3.4. Notices 201 | 202 | You may not remove or alter the substance of any license notices 203 | (including copyright notices, patent notices, disclaimers of warranty, 204 | or limitations of liability) contained within the Source Code Form of 205 | the Covered Software, except that You may alter any license notices to 206 | the extent required to remedy known factual inaccuracies. 207 | 208 | 3.5. Application of Additional Terms 209 | 210 | You may choose to offer, and to charge a fee for, warranty, support, 211 | indemnity or liability obligations to one or more recipients of Covered 212 | Software. However, You may do so only on Your own behalf, and not on 213 | behalf of any Contributor. You must make it absolutely clear that any 214 | such warranty, support, indemnity, or liability obligation is offered by 215 | You alone, and You hereby agree to indemnify every Contributor for any 216 | liability incurred by such Contributor as a result of warranty, support, 217 | indemnity or liability terms You offer. You may include additional 218 | disclaimers of warranty and limitations of liability specific to any 219 | jurisdiction. 220 | 221 | 4. Inability to Comply Due to Statute or Regulation 222 | --------------------------------------------------- 223 | 224 | If it is impossible for You to comply with any of the terms of this 225 | License with respect to some or all of the Covered Software due to 226 | statute, judicial order, or regulation then You must: (a) comply with 227 | the terms of this License to the maximum extent possible; and (b) 228 | describe the limitations and the code they affect. Such description must 229 | be placed in a text file included with all distributions of the Covered 230 | Software under this License. Except to the extent prohibited by statute 231 | or regulation, such description must be sufficiently detailed for a 232 | recipient of ordinary skill to be able to understand it. 233 | 234 | 5. Termination 235 | -------------- 236 | 237 | 5.1. The rights granted under this License will terminate automatically 238 | if You fail to comply with any of its terms. However, if You become 239 | compliant, then the rights granted under this License from a particular 240 | Contributor are reinstated (a) provisionally, unless and until such 241 | Contributor explicitly and finally terminates Your grants, and (b) on an 242 | ongoing basis, if such Contributor fails to notify You of the 243 | non-compliance by some reasonable means prior to 60 days after You have 244 | come back into compliance. Moreover, Your grants from a particular 245 | Contributor are reinstated on an ongoing basis if such Contributor 246 | notifies You of the non-compliance by some reasonable means, this is the 247 | first time You have received notice of non-compliance with this License 248 | from such Contributor, and You become compliant prior to 30 days after 249 | Your receipt of the notice. 250 | 251 | 5.2. If You initiate litigation against any entity by asserting a patent 252 | infringement claim (excluding declaratory judgment actions, 253 | counter-claims, and cross-claims) alleging that a Contributor Version 254 | directly or indirectly infringes any patent, then the rights granted to 255 | You by any and all Contributors for the Covered Software under Section 256 | 2.1 of this License shall terminate. 257 | 258 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 259 | end user license agreements (excluding distributors and resellers) which 260 | have been validly granted by You or Your distributors under this License 261 | prior to termination shall survive termination. 262 | 263 | ************************************************************************ 264 | * * 265 | * 6. Disclaimer of Warranty * 266 | * ------------------------- * 267 | * * 268 | * Covered Software is provided under this License on an "as is" * 269 | * basis, without warranty of any kind, either expressed, implied, or * 270 | * statutory, including, without limitation, warranties that the * 271 | * Covered Software is free of defects, merchantable, fit for a * 272 | * particular purpose or non-infringing. The entire risk as to the * 273 | * quality and performance of the Covered Software is with You. * 274 | * Should any Covered Software prove defective in any respect, You * 275 | * (not any Contributor) assume the cost of any necessary servicing, * 276 | * repair, or correction. This disclaimer of warranty constitutes an * 277 | * essential part of this License. No use of any Covered Software is * 278 | * authorized under this License except under this disclaimer. * 279 | * * 280 | ************************************************************************ 281 | 282 | ************************************************************************ 283 | * * 284 | * 7. Limitation of Liability * 285 | * -------------------------- * 286 | * * 287 | * Under no circumstances and under no legal theory, whether tort * 288 | * (including negligence), contract, or otherwise, shall any * 289 | * Contributor, or anyone who distributes Covered Software as * 290 | * permitted above, be liable to You for any direct, indirect, * 291 | * special, incidental, or consequential damages of any character * 292 | * including, without limitation, damages for lost profits, loss of * 293 | * goodwill, work stoppage, computer failure or malfunction, or any * 294 | * and all other commercial damages or losses, even if such party * 295 | * shall have been informed of the possibility of such damages. This * 296 | * limitation of liability shall not apply to liability for death or * 297 | * personal injury resulting from such party's negligence to the * 298 | * extent applicable law prohibits such limitation. Some * 299 | * jurisdictions do not allow the exclusion or limitation of * 300 | * incidental or consequential damages, so this exclusion and * 301 | * limitation may not apply to You. * 302 | * * 303 | ************************************************************************ 304 | 305 | 8. Litigation 306 | ------------- 307 | 308 | Any litigation relating to this License may be brought only in the 309 | courts of a jurisdiction where the defendant maintains its principal 310 | place of business and such litigation shall be governed by laws of that 311 | jurisdiction, without reference to its conflict-of-law provisions. 312 | Nothing in this Section shall prevent a party's ability to bring 313 | cross-claims or counter-claims. 314 | 315 | 9. Miscellaneous 316 | ---------------- 317 | 318 | This License represents the complete agreement concerning the subject 319 | matter hereof. If any provision of this License is held to be 320 | unenforceable, such provision shall be reformed only to the extent 321 | necessary to make it enforceable. Any law or regulation which provides 322 | that the language of a contract shall be construed against the drafter 323 | shall not be used to construe this License against a Contributor. 324 | 325 | 10. Versions of the License 326 | --------------------------- 327 | 328 | 10.1. New Versions 329 | 330 | Mozilla Foundation is the license steward. Except as provided in Section 331 | 10.3, no one other than the license steward has the right to modify or 332 | publish new versions of this License. Each version will be given a 333 | distinguishing version number. 334 | 335 | 10.2. Effect of New Versions 336 | 337 | You may distribute the Covered Software under the terms of the version 338 | of the License under which You originally received the Covered Software, 339 | or under the terms of any subsequent version published by the license 340 | steward. 341 | 342 | 10.3. Modified Versions 343 | 344 | If you create software not governed by this License, and you want to 345 | create a new license for such software, you may create and use a 346 | modified version of this License if you rename the license and remove 347 | any references to the name of the license steward (except to note that 348 | such modified license differs from this License). 349 | 350 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 351 | Licenses 352 | 353 | If You choose to distribute Source Code Form that is Incompatible With 354 | Secondary Licenses under the terms of this version of the License, the 355 | notice described in Exhibit B of this License must be attached. 356 | 357 | Exhibit A - Source Code Form License Notice 358 | ------------------------------------------- 359 | 360 | This Source Code Form is subject to the terms of the Mozilla Public 361 | License, v. 2.0. If a copy of the MPL was not distributed with this 362 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 363 | 364 | If it is not possible or desirable to put the notice in a particular 365 | file, then You may include the notice in a location (such as a LICENSE 366 | file in a relevant directory) where a recipient would be likely to look 367 | for such a notice. 368 | 369 | You may add additional accurate notices of copyright ownership. 370 | 371 | Exhibit B - "Incompatible With Secondary Licenses" Notice 372 | --------------------------------------------------------- 373 | 374 | This Source Code Form is "Incompatible With Secondary Licenses", as 375 | defined by the Mozilla Public License, v. 2.0. 376 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOOS?=linux 2 | GOARCH?=amd64 3 | TERRAFORM_ARGS= 4 | PKG=github.com/hashicorp/vault-lambda-extension/internal/config 5 | VERSION?=0.0.0-dev 6 | .PHONY: dev build zip lint test clean mod quick-start quick-start-destroy publish-layer-version 7 | 8 | dev: build 9 | 10 | build: clean 11 | GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=0 go build \ 12 | -ldflags "-s -w -X '$(PKG).ExtensionVersion=$(VERSION)'" \ 13 | -a -o pkg/extensions/vault-lambda-extension \ 14 | . 15 | 16 | zip: build 17 | cp LICENSE pkg/LICENSE.txt 18 | cd pkg && zip -r vault-lambda-extension.zip LICENSE.txt extensions/ 19 | @echo "Extension built: pkg/vault-lambda-extension.zip" 20 | 21 | lint: 22 | golangci-lint run -v --concurrency 2 \ 23 | --disable-all \ 24 | --timeout 10m \ 25 | --enable gofmt \ 26 | --enable gosimple \ 27 | --enable govet 28 | 29 | test: 30 | CGO_ENABLED=0 go test -v ./... -timeout=20m 31 | 32 | clean: 33 | -rm -rf pkg 34 | 35 | mod: 36 | @go mod tidy 37 | 38 | quick-start: 39 | bash quick-start/build.sh 40 | cd quick-start/terraform && \ 41 | terraform init && \ 42 | terraform apply -auto-approve $(TERRAFORM_ARGS) 43 | aws lambda invoke --function-name vault-lambda-extension-demo-function /dev/null \ 44 | --cli-binary-format raw-in-base64-out \ 45 | --log-type Tail \ 46 | --region us-east-1 \ 47 | | jq -r '.LogResult' \ 48 | | base64 --decode 49 | 50 | quick-start-destroy: 51 | cd quick-start/terraform && \ 52 | terraform destroy -auto-approve 53 | 54 | publish-layer-version: zip 55 | aws lambda publish-layer-version \ 56 | --layer-name "vault-lambda-extension" \ 57 | --zip-file "fileb://pkg/vault-lambda-extension.zip" \ 58 | --region "us-east-1" \ 59 | --no-cli-pager \ 60 | --output text \ 61 | --query LayerVersionArn 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vault-lambda-extension 2 | 3 | ---- 4 | 5 | **Please note**: We take Vault's security and our users' trust very seriously. If you believe you have found a security issue in Vault or vault-lambda-extension, _please responsibly disclose_ by contacting us at [security@hashicorp.com](mailto:security@hashicorp.com). 6 | 7 | ---- 8 | 9 | This repository contains the source code for HashiCorp's Vault AWS Lambda extension. 10 | The extension utilizes the AWS Lambda Extensions API to help your Lambda function 11 | read secrets from your Vault deployment. 12 | 13 | ## Usage 14 | 15 | To use the extension, include one of the following ARNs as a layer in your 16 | Lambda function, depending on your desired architecture. 17 | 18 | amd64 (x86_64): 19 | 20 | ```text 21 | arn:aws:lambda::634166935893:layer:vault-lambda-extension:22 22 | ``` 23 | 24 | arm64: 25 | 26 | ```text 27 | arn:aws:lambda::634166935893:layer:vault-lambda-extension-arm64:10 28 | ``` 29 | 30 | Where region may be any of 31 | * `af-south-1` 32 | * `ap-east-1` 33 | * `ap-northeast-1` 34 | * `ap-northeast-2` 35 | * `ap-northeast-3` 36 | * `ap-south-1` 37 | * `ap-south-2` 38 | * `ap-southeast-1` 39 | * `ap-southeast-2` 40 | * `ca-central-1` 41 | * `eu-central-1` 42 | * `eu-north-1` 43 | * `eu-south-1` 44 | * `eu-west-1` 45 | * `eu-west-2` 46 | * `eu-west-3` 47 | * `me-south-1` 48 | * `sa-east-1` 49 | * `us-east-1` 50 | * `us-east-2` 51 | * `us-west-1` 52 | * `us-west-2` 53 | 54 | Alternatively, you can download binaries for packaging into a container image 55 | [here][releases]. See the full [documentation page][vault-docs] for more details. 56 | 57 | The extension authenticates with Vault using [AWS IAM auth][vault-aws-iam-auth], 58 | and all configuration is supplied via environment variables. There are two methods 59 | to read secrets, which can both be used side-by-side: 60 | 61 | * **Recommended**: Make unauthenticated requests to the extension's local proxy 62 | server at `http://127.0.0.1:8200`, which will add an authentication header and 63 | proxy to the configured `VAULT_ADDR`. Responses from Vault are returned without 64 | modification. 65 | * Configure environment variables such as `VAULT_SECRET_PATH` for the extension 66 | to read a secret and write it to disk. 67 | 68 | ## Getting Started 69 | 70 | The [learn guide][vault-learn-guide] is the most complete and fully explained 71 | tutorial on getting started from scratch. Alternatively, you can follow the 72 | similar quick start guide below or see the instructions for adding the extension 73 | to your existing function. General [usage documentation][vault-docs] is also 74 | available. 75 | 76 | ### Quick Start 77 | 78 | The [quick-start](./quick-start) directory has an end to end example, for which 79 | you will need an AWS account and some command line tools. Follow the readme in 80 | that directory if you'd like to try out the extension from scratch. **Please 81 | note it will create real infrastructure with an associated cost as per AWS' 82 | pricing.** 83 | 84 | ## Testing 85 | 86 | If you want to test changes to the lambda extension, you can build and deploy the local version for testing with the Quick Start: 87 | 88 | ```sh 89 | make zip 90 | make quick-start TERRAFORM_ARGS="-var local_extension=true" 91 | ``` 92 | 93 | There is also a terraform variable for using an additional IAM role for the Lambda to assume: 94 | 95 | ``` 96 | make zip 97 | make quick-start TERRAFORM_ARGS="-var local_extension=true -var assume_role=true" 98 | ``` 99 | 100 | [vault-learn-guide]: https://learn.hashicorp.com/tutorials/vault/aws-lambda 101 | [vault-docs]: https://developer.hashicorp.com/vault/docs/platform/aws/lambda-extension 102 | [vault-aws-iam-auth]: https://developer.hashicorp.com/vault/docs/auth/aws 103 | [releases]: https://releases.hashicorp.com/vault-lambda-extension/ 104 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/vault-lambda-extension 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.55.6 7 | github.com/hashicorp/go-hclog v1.6.3 8 | github.com/hashicorp/go-multierror v1.1.1 9 | github.com/hashicorp/vault/api v1.15.0 10 | github.com/hashicorp/vault/sdk v0.15.0 11 | github.com/patrickmn/go-cache v2.1.0+incompatible 12 | github.com/stretchr/testify v1.10.0 13 | ) 14 | 15 | require ( 16 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 17 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 18 | github.com/fatih/color v1.17.0 // indirect 19 | github.com/go-jose/go-jose/v4 v4.0.5 // indirect 20 | github.com/hashicorp/errwrap v1.1.0 // indirect 21 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 22 | github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6 // indirect 23 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 24 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 25 | github.com/hashicorp/go-secure-stdlib/cryptoutil v0.1.1 // indirect 26 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9 // indirect 27 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 28 | github.com/hashicorp/go-sockaddr v1.0.6 // indirect 29 | github.com/hashicorp/hcl v1.0.1-vault-5 // indirect 30 | github.com/jmespath/go-jmespath v0.4.0 // indirect 31 | github.com/mattn/go-colorable v0.1.13 // indirect 32 | github.com/mattn/go-isatty v0.0.20 // indirect 33 | github.com/mitchellh/go-homedir v1.1.0 // indirect 34 | github.com/mitchellh/mapstructure v1.5.0 // indirect 35 | github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect 36 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 37 | github.com/ryanuber/go-glob v1.0.0 // indirect 38 | github.com/sasha-s/go-deadlock v0.3.5 // indirect 39 | golang.org/x/crypto v0.36.0 // indirect 40 | golang.org/x/net v0.38.0 // indirect 41 | golang.org/x/sys v0.31.0 // indirect 42 | golang.org/x/text v0.23.0 // indirect 43 | golang.org/x/time v0.9.0 // indirect 44 | gopkg.in/yaml.v3 v3.0.1 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= 2 | github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= 3 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 4 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 8 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 10 | github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 11 | github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 12 | github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= 13 | github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= 14 | github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= 15 | github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 16 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 17 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 18 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 19 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 20 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 21 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 22 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 23 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 24 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 25 | github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6 h1:kBoJV4Xl5FLtBfnBjDvBxeNSy2IRITSGs73HQsFUEjY= 26 | github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6/go.mod h1:y+HSOcOGB48PkUxNyLAiCiY6rEENu+E+Ss4LG8QHwf4= 27 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 28 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 29 | github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= 30 | github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= 31 | github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= 32 | github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= 33 | github.com/hashicorp/go-secure-stdlib/cryptoutil v0.1.1 h1:VaLXp47MqD1Y2K6QVrA9RooQiPyCgAbnfeJg44wKuJk= 34 | github.com/hashicorp/go-secure-stdlib/cryptoutil v0.1.1/go.mod h1:hH8rgXHh9fPSDPerG6WzABHsHF+9ZpLhRI1LPk4JZ8c= 35 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9 h1:FW0YttEnUNDJ2WL9XcrrfteS1xW8u+sh4ggM8pN5isQ= 36 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= 37 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= 38 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= 39 | github.com/hashicorp/go-sockaddr v1.0.6 h1:RSG8rKU28VTUTvEKghe5gIhIQpv8evvNpnDEyqO4u9I= 40 | github.com/hashicorp/go-sockaddr v1.0.6/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI= 41 | github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= 42 | github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= 43 | github.com/hashicorp/vault/api v1.15.0 h1:O24FYQCWwhwKnF7CuSqP30S51rTV7vz1iACXE/pj5DA= 44 | github.com/hashicorp/vault/api v1.15.0/go.mod h1:+5YTO09JGn0u+b6ySD/LLVf8WkJCPLAL2Vkmrn2+CM8= 45 | github.com/hashicorp/vault/sdk v0.15.0 h1:xNo1lL2shm0yE4coXNZkTV/6++2GfEh+/cCAfBjzEnA= 46 | github.com/hashicorp/vault/sdk v0.15.0/go.mod h1:2Wj2tHIgfz0gNWgEPWBbCXFIiPrq96E8FTjPNV9J1Bc= 47 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 48 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 49 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 50 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 51 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 52 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 53 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 54 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 55 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 56 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 57 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 58 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 59 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 60 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 61 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 62 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 63 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 64 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 65 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 66 | github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= 67 | github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= 68 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 69 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 70 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 71 | github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= 72 | github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 73 | github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= 74 | github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= 75 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 76 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 77 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 78 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 79 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 80 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 81 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 82 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 83 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 84 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 85 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 86 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 87 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 88 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 89 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 90 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 91 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 92 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 93 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 94 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 95 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 96 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 97 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 98 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 99 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 100 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 101 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 102 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 103 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 104 | -------------------------------------------------------------------------------- /internal/config/auth.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package config 5 | 6 | import ( 7 | "os" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | vaultAuthRole = "VAULT_AUTH_ROLE" 13 | vaultAuthProvider = "VAULT_AUTH_PROVIDER" 14 | vaultAssumedRoleArn = "VAULT_ASSUMED_ROLE_ARN" // Optional 15 | vaultIAMServerID = "VAULT_IAM_SERVER_ID" // Optional 16 | vleVaultAddr = "VLE_VAULT_ADDR" // Optional, overrides VAULT_ADDR 17 | stsEndpointRegionEnv = "VAULT_STS_ENDPOINT_REGION" // Optional 18 | ) 19 | 20 | // AuthConfig holds config required for logging in to Vault. 21 | type AuthConfig struct { 22 | Role string 23 | Provider string 24 | AssumedRoleArn string 25 | IAMServerID string 26 | STSEndpointRegion string 27 | VaultAddress string 28 | } 29 | 30 | // AuthConfigFromEnv reads config from the environment for authenticating to Vault. 31 | func AuthConfigFromEnv() AuthConfig { 32 | return AuthConfig{ 33 | Role: strings.TrimSpace(os.Getenv(vaultAuthRole)), 34 | Provider: strings.TrimSpace(os.Getenv(vaultAuthProvider)), 35 | AssumedRoleArn: strings.TrimSpace(os.Getenv(vaultAssumedRoleArn)), 36 | IAMServerID: strings.TrimSpace(os.Getenv(vaultIAMServerID)), 37 | STSEndpointRegion: strings.TrimSpace(os.Getenv(stsEndpointRegionEnv)), 38 | VaultAddress: strings.TrimSpace(os.Getenv(vleVaultAddr)), 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/config/cache.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package config 5 | 6 | import ( 7 | "os" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | const ( 14 | // The time to live configuration (aka, TTL) of the cache used by proxy 15 | // server. 16 | VaultCacheTTL = "VAULT_DEFAULT_CACHE_TTL" 17 | 18 | // When set to `true`, every request will be saved in the cache and returned 19 | // from cache, making caching "opt-out" instead of "opt-in". Caching may 20 | // still be disabled per-request with the "nocache" cache-control header. 21 | VaultCacheEnabled = "VAULT_DEFAULT_CACHE_ENABLED" 22 | ) 23 | 24 | // CacheConfig holds config for the request cache 25 | type CacheConfig struct { 26 | TTL time.Duration 27 | DefaultEnabled bool 28 | } 29 | 30 | // CacheConfigFromEnv reads config from the environment for caching 31 | func CacheConfigFromEnv() CacheConfig { 32 | var cacheTTL time.Duration 33 | cacheTTLEnv := strings.TrimSpace(os.Getenv(VaultCacheTTL)) 34 | if cacheTTLEnv != "" { 35 | var err error 36 | cacheTTL, err = time.ParseDuration(cacheTTLEnv) 37 | if err != nil { 38 | cacheTTL = 0 39 | } 40 | } 41 | 42 | defaultOn := false 43 | defaultOnEnv := strings.TrimSpace(os.Getenv(VaultCacheEnabled)) 44 | if defaultOnEnv != "" { 45 | var err error 46 | defaultOn, err = strconv.ParseBool(defaultOnEnv) 47 | if err != nil { 48 | defaultOn = false 49 | } 50 | } 51 | 52 | return CacheConfig{ 53 | TTL: cacheTTL, 54 | DefaultEnabled: defaultOn, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/config/cache_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package config 5 | 6 | import ( 7 | "os" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestCacheConfig(t *testing.T) { 16 | t.Run("Valid vault cache TTL", func(t *testing.T) { 17 | defer os.Unsetenv(VaultCacheTTL) 18 | ttlArray := []string{"15m", "2s", "1h3m", "0h2m3s", "1h2m3s", "15s"} 19 | for _, ttl := range ttlArray { 20 | os.Setenv(VaultCacheTTL, ttl) 21 | cacheConfig := CacheConfigFromEnv() 22 | expectedTTL, err := time.ParseDuration(ttl) 23 | require.NoError(t, err) 24 | assert.Equal(t, expectedTTL, cacheConfig.TTL) 25 | assert.False(t, cacheConfig.DefaultEnabled) 26 | } 27 | }) 28 | 29 | t.Run("Invalid vault cache TTL", func(t *testing.T) { 30 | defer os.Unsetenv(VaultCacheTTL) 31 | ttlArray := []string{"15sm", "2st", "1h3m5t", "-0h2m3s", "15", "-15s"} 32 | for _, ttl := range ttlArray { 33 | os.Setenv(VaultCacheTTL, ttl) 34 | cacheConfig := CacheConfigFromEnv() 35 | assert.LessOrEqual(t, cacheConfig.TTL, int64(0)) 36 | } 37 | }) 38 | 39 | t.Run("Valid vault default cache enabled", func(t *testing.T) { 40 | defer os.Unsetenv(VaultCacheTTL) 41 | defer os.Unsetenv(VaultCacheEnabled) 42 | enabled := []string{"true", "t", "1", "True"} 43 | for _, e := range enabled { 44 | os.Setenv(VaultCacheTTL, "5m") 45 | os.Setenv(VaultCacheEnabled, e) 46 | cacheConfig := CacheConfigFromEnv() 47 | assert.True(t, cacheConfig.DefaultEnabled) 48 | } 49 | }) 50 | 51 | t.Run("False or invalid vault default cache enabled shall result in DefaultEnabled=false", func(t *testing.T) { 52 | defer os.Unsetenv(VaultCacheTTL) 53 | defer os.Unsetenv(VaultCacheEnabled) 54 | enabled := []string{"false", "f", "0", "False", "no", "nocache"} 55 | for _, e := range enabled { 56 | os.Setenv(VaultCacheTTL, "5m") 57 | os.Setenv(VaultCacheEnabled, e) 58 | cacheConfig := CacheConfigFromEnv() 59 | assert.False(t, cacheConfig.DefaultEnabled) 60 | } 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /internal/config/info.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package config 5 | 6 | const ( 7 | ExtensionName = "vault-lambda-extension" 8 | VaultLogLevel = "VAULT_LOG_LEVEL" // Optional, one of TRACE, DEBUG, INFO, WARN, ERROR, OFF 9 | VaultRunMode = "VAULT_RUN_MODE" 10 | ) 11 | 12 | var ( 13 | // ExtensionVersion should be a var type, so the go build tool can override and inject a custom version. 14 | ExtensionVersion = "0.0.0-dev" 15 | ) 16 | -------------------------------------------------------------------------------- /internal/config/secret.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package config 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path" 10 | "strings" 11 | 12 | "github.com/hashicorp/go-multierror" 13 | ) 14 | 15 | const ( 16 | vaultSecretPathKey = "VAULT_SECRET_PATH" 17 | vaultSecretFileKey = "VAULT_SECRET_FILE" 18 | vaultSecretPathPrefix = vaultSecretPathKey + "_" 19 | vaultSecretFilePrefix = vaultSecretFileKey + "_" 20 | 21 | DefaultSecretDirectory = "/tmp/vault" 22 | DefaultSecretFile = "secret.json" 23 | ) 24 | 25 | var ( 26 | // For the purposes of mocking in tests 27 | getenv = os.Getenv 28 | environ = os.Environ 29 | ) 30 | 31 | // ConfiguredSecret represents a pair of environment variables of the form: 32 | // 33 | // VAULT_SECRET_PATH_FOO=/kv/data/foo 34 | // VAULT_SECRET_FILE_FOO=/tmp/vault/secret/foo 35 | // 36 | // Where FOO is the name, and must match across both env vars to form a 37 | // valid secret configuration. The name can also be empty. 38 | type ConfiguredSecret struct { 39 | name string // The name assigned to the secret 40 | 41 | VaultPath string // The path to read from in Vault 42 | FilePath string // The path to write to in the file system 43 | } 44 | 45 | // Valid checks that both a secret path and a destination path are given. 46 | func (cs ConfiguredSecret) Valid() bool { 47 | return cs.VaultPath != "" && cs.FilePath != "" 48 | } 49 | 50 | // Name is the name parsed from the environment variable name. This name is used 51 | // as a key to match secrets with file paths. 52 | func (cs ConfiguredSecret) Name() string { 53 | if cs.name == "" { 54 | return "" 55 | } 56 | 57 | return cs.name 58 | } 59 | 60 | // ParseConfiguredSecrets reads environment variables to determine which secrets 61 | // to read from Vault, and where to write them on disk. 62 | func ParseConfiguredSecrets() ([]ConfiguredSecret, error) { 63 | envVars := environ() 64 | secrets := make(map[string]*ConfiguredSecret) 65 | var resultErr error 66 | 67 | for _, kv := range envVars { 68 | parts := strings.SplitN(kv, "=", 2) 69 | if len(parts) != 2 { 70 | // This should never happen. 71 | return nil, fmt.Errorf("os.Environ should return key=value pairs, but got %s", kv) 72 | } 73 | key := parts[0] 74 | value := strings.TrimSpace(parts[1]) 75 | 76 | switch { 77 | case strings.HasPrefix(key, vaultSecretPathPrefix): 78 | name := key[len(vaultSecretPathPrefix):] 79 | if name == "" { 80 | resultErr = multierror.Append(resultErr, fmt.Errorf("%s is not valid configuration; specify %s for a nameless secret or specify a non-zero length name", vaultSecretPathPrefix, vaultSecretPathKey)) 81 | break 82 | } 83 | if s, exists := secrets[name]; exists { 84 | s.VaultPath = value 85 | } else { 86 | secrets[name] = &ConfiguredSecret{ 87 | name: name, 88 | VaultPath: value, 89 | } 90 | } 91 | 92 | case strings.HasPrefix(key, vaultSecretFilePrefix): 93 | name := key[len(vaultSecretFilePrefix):] 94 | if name == "" { 95 | resultErr = multierror.Append(resultErr, fmt.Errorf("%s is not valid configuration; specify %s for a nameless secret or specify a non-zero length name", vaultSecretFilePrefix, vaultSecretFileKey)) 96 | break 97 | } 98 | filePath := filePathFromEnv(value) 99 | if s, exists := secrets[name]; exists { 100 | s.FilePath = filePath 101 | } else { 102 | secrets[name] = &ConfiguredSecret{ 103 | name: name, 104 | FilePath: filePath, 105 | } 106 | } 107 | } 108 | } 109 | 110 | // Special case for anonymous-name secret 111 | anonymousSecretVaultPath := strings.TrimSpace(getenv(vaultSecretPathKey)) 112 | if anonymousSecretVaultPath != "" { 113 | s := &ConfiguredSecret{ 114 | name: "", 115 | VaultPath: anonymousSecretVaultPath, 116 | FilePath: filePathFromEnv(strings.TrimSpace(getenv(vaultSecretFileKey))), 117 | } 118 | if s.FilePath == "" { 119 | s.FilePath = path.Join(DefaultSecretDirectory, DefaultSecretFile) 120 | } 121 | secrets[""] = s 122 | } 123 | 124 | // Track files we will write to check for clashes. 125 | fileLocations := make(map[string]*ConfiguredSecret) 126 | result := make([]ConfiguredSecret, 0) 127 | for _, secret := range secrets { 128 | if !secret.Valid() { 129 | resultErr = multierror.Append(resultErr, fmt.Errorf("invalid secret (must have both a path and a file specified): path=%q, file=%q", secret.VaultPath, secret.FilePath)) 130 | continue 131 | } 132 | if processedSecret, clash := fileLocations[secret.FilePath]; clash { 133 | resultErr = multierror.Append(resultErr, fmt.Errorf("two secrets, %q and %q, are configured to write to the same location on disk: %s", processedSecret.Name(), secret.Name(), secret.FilePath)) 134 | continue 135 | } 136 | fileLocations[secret.FilePath] = secret 137 | 138 | result = append(result, *secret) 139 | } 140 | 141 | return result, resultErr 142 | } 143 | 144 | func filePathFromEnv(envFilePath string) string { 145 | if envFilePath == "" { 146 | return "" 147 | } 148 | if path.IsAbs(envFilePath) { 149 | return envFilePath 150 | } 151 | 152 | return path.Join(DefaultSecretDirectory, envFilePath) 153 | } 154 | -------------------------------------------------------------------------------- /internal/config/secret_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package config 5 | 6 | import ( 7 | "fmt" 8 | "sort" 9 | "testing" 10 | 11 | "github.com/hashicorp/go-multierror" 12 | ) 13 | 14 | func TestParseConfiguredSecrets(t *testing.T) { 15 | for _, tc := range []struct { 16 | name string 17 | env map[string]string 18 | expected []ConfiguredSecret 19 | expectErrors int 20 | }{ 21 | { 22 | name: "Empty environment", 23 | expected: []ConfiguredSecret{}, 24 | expectErrors: 0, 25 | }, 26 | { 27 | name: "Minimal valid config", 28 | env: map[string]string{ 29 | "VAULT_SECRET_PATH": "/kv/data/foo", 30 | }, 31 | expected: []ConfiguredSecret{ 32 | { 33 | name: "", 34 | VaultPath: "/kv/data/foo", 35 | FilePath: "/tmp/vault/secret.json", 36 | }, 37 | }, 38 | }, 39 | { 40 | name: "1 secret - no name", 41 | env: map[string]string{ 42 | "VAULT_SECRET_PATH": "/kv/data/foo", 43 | "VAULT_SECRET_FILE": "/tmp/vault/secret/foo", 44 | }, 45 | expected: []ConfiguredSecret{ 46 | { 47 | name: "", 48 | VaultPath: "/kv/data/foo", 49 | FilePath: "/tmp/vault/secret/foo", 50 | }, 51 | }, 52 | }, 53 | { 54 | name: "2 secrets", 55 | env: map[string]string{ 56 | "VAULT_SECRET_PATH": "/kv/data/foo", 57 | "VAULT_SECRET_FILE": "/tmp/vault/secret/foo", 58 | "VAULT_SECRET_PATH_FOO": "FOO vaultPath", 59 | "VAULT_SECRET_FILE_FOO": "/FOO/file/path", 60 | }, 61 | expected: []ConfiguredSecret{ 62 | { 63 | name: "", 64 | VaultPath: "/kv/data/foo", 65 | FilePath: "/tmp/vault/secret/foo", 66 | }, 67 | { 68 | name: "FOO", 69 | VaultPath: "FOO vaultPath", 70 | FilePath: "/FOO/file/path", 71 | }, 72 | }, 73 | }, 74 | { 75 | name: "Absolute vs relative paths", 76 | env: map[string]string{ 77 | "VAULT_SECRET_PATH": "default location", 78 | "VAULT_SECRET_PATH_ABSOLUTE": "a", 79 | "VAULT_SECRET_FILE_ABSOLUTE": "/somewhere/else/completely", 80 | "VAULT_SECRET_PATH_RELATIVE": "a", 81 | "VAULT_SECRET_FILE_RELATIVE": "my-special-location.yaml", 82 | }, 83 | expected: []ConfiguredSecret{ 84 | { 85 | name: "", 86 | VaultPath: "default location", 87 | FilePath: "/tmp/vault/secret.json", 88 | }, 89 | { 90 | name: "ABSOLUTE", 91 | VaultPath: "a", 92 | FilePath: "/somewhere/else/completely", 93 | }, 94 | { 95 | name: "RELATIVE", 96 | VaultPath: "a", 97 | FilePath: "/tmp/vault/my-special-location.yaml", 98 | }, 99 | }, 100 | }, 101 | { 102 | name: "Misconfigured secrets", 103 | env: map[string]string{ 104 | "VAULT_SECRET_PATH_FOO": "a", // No VAULT_SECRET_FILE_FOO env var 105 | "VAULT_SECRET_PATH_BAR": "a", // No VAULT_SECRET_PATH_BAR env var 106 | "VAULT_SECRET_PATH_": "invalid name", 107 | "VAULT_SECRET_FILE_": "invalid name", 108 | "VAULT_SECRET_PATH": "a", 109 | "VAULT_SECRET_PATH_DUP_PATH": "a", 110 | "VAULT_SECRET_FILE_DUP_PATH": "/tmp/vault/secret.json", // Writes to the same path as the anonymous secret 111 | }, 112 | expected: []ConfiguredSecret{}, 113 | expectErrors: 5, 114 | }, 115 | } { 116 | t.Run(tc.name, func(t *testing.T) { 117 | setenv(tc.env) 118 | secrets, err := ParseConfiguredSecrets() 119 | if err != nil { 120 | if tc.expectErrors == 0 { 121 | t.Fatalf("Expected no errors, but got: %s", err) 122 | } 123 | if merr, ok := err.(*multierror.Error); ok { 124 | if len(merr.Errors) != tc.expectErrors { 125 | t.Fatalf("Expected %d error(s) but got %d: %s", tc.expectErrors, len(merr.Errors), err) 126 | } 127 | } else if tc.expectErrors != 1 { 128 | t.Fatalf("Expected %d errors but got 1", tc.expectErrors) 129 | } 130 | } 131 | if err == nil && tc.expectErrors > 0 { 132 | t.Fatalf("Expected %d errors but got none", tc.expectErrors) 133 | } 134 | 135 | if tc.expectErrors > 0 { 136 | return 137 | } 138 | 139 | if len(secrets) != len(tc.expected) { 140 | t.Fatalf("Expected %d secret(s), but got %d: %+v", len(tc.expected), len(secrets), secrets) 141 | } 142 | sort.Slice(secrets, func(i, j int) bool { 143 | return secrets[i].name < secrets[j].name 144 | }) 145 | for i, s := range secrets { 146 | if s != tc.expected[i] { 147 | t.Fatalf("Expected secret %+v but got %+v", tc.expected[i], s) 148 | } 149 | } 150 | }) 151 | } 152 | } 153 | 154 | func setenv(env map[string]string) { 155 | getenv = func(k string) string { 156 | return env[k] 157 | } 158 | environ = func() []string { 159 | result := make([]string, 0, len(env)) 160 | for k, v := range env { 161 | result = append(result, fmt.Sprintf("%s=%s", k, v)) 162 | } 163 | return result 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /internal/config/useragent.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package config 5 | 6 | import ( 7 | "fmt" 8 | "runtime" 9 | ) 10 | 11 | // GetUserAgentBase returns a base user agent string with the given user agent name and version in the form: 12 | // vault-client-go/0.0.1 (Darwin arm64; Go go1.19.2) 13 | func GetUserAgentBase(clientName string, clientVersion string) string { 14 | return fmt.Sprintf("%s/%s (%s %s; Go %s)", clientName, clientVersion, runtime.GOOS, runtime.GOARCH, runtime.Version()) 15 | } 16 | -------------------------------------------------------------------------------- /internal/extension/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package extension 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "net/http" 13 | ) 14 | 15 | // RegisterResponse is the body of the response for /register 16 | type RegisterResponse struct { 17 | FunctionName string `json:"functionName"` 18 | FunctionVersion string `json:"functionVersion"` 19 | Handler string `json:"handler"` 20 | Configuration map[string]string `json:"configuration"` 21 | } 22 | 23 | // NextEventResponse is the response for /event/next 24 | type NextEventResponse struct { 25 | EventType EventType `json:"eventType"` 26 | DeadlineMs int64 `json:"deadlineMs"` 27 | RequestID string `json:"requestId"` 28 | InvokedFunctionArn string `json:"invokedFunctionArn"` 29 | Tracing Tracing `json:"tracing"` 30 | } 31 | 32 | // Tracing is part of the response for /event/next 33 | type Tracing struct { 34 | Type string `json:"type"` 35 | Value string `json:"value"` 36 | } 37 | 38 | // EventType represents the type of events recieved from /event/next 39 | type EventType string 40 | 41 | const ( 42 | // Invoke is a lambda invoke 43 | Invoke EventType = "INVOKE" 44 | 45 | // Shutdown is a shutdown event for the environment 46 | Shutdown EventType = "SHUTDOWN" 47 | 48 | extensionNameHeader = "Lambda-Extension-Name" 49 | extensionIdentiferHeader = "Lambda-Extension-Identifier" 50 | ) 51 | 52 | // Client is a simple client for the Lambda Extensions API 53 | type Client struct { 54 | baseURL string 55 | httpClient *http.Client 56 | extensionID string 57 | } 58 | 59 | // NewClient returns a Lambda Extensions API client 60 | func NewClient(awsLambdaRuntimeAPI string) *Client { 61 | baseURL := fmt.Sprintf("http://%s/2020-01-01/extension", awsLambdaRuntimeAPI) 62 | return &Client{ 63 | baseURL: baseURL, 64 | httpClient: &http.Client{}, 65 | } 66 | } 67 | 68 | // Register will register the extension with the Extensions API 69 | func (e *Client) Register(ctx context.Context, filename string) (*RegisterResponse, error) { 70 | const action = "/register" 71 | url := e.baseURL + action 72 | 73 | reqBody, err := json.Marshal(map[string]interface{}{ 74 | "events": []EventType{Invoke, Shutdown}, 75 | }) 76 | if err != nil { 77 | return nil, err 78 | } 79 | httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(reqBody)) 80 | if err != nil { 81 | return nil, err 82 | } 83 | httpReq.Header.Set(extensionNameHeader, filename) 84 | httpRes, err := e.httpClient.Do(httpReq) 85 | if err != nil { 86 | return nil, err 87 | } 88 | if httpRes.StatusCode != 200 { 89 | return nil, fmt.Errorf("request failed with status %s", httpRes.Status) 90 | } 91 | defer httpRes.Body.Close() 92 | body, err := io.ReadAll(httpRes.Body) 93 | if err != nil { 94 | return nil, err 95 | } 96 | res := RegisterResponse{} 97 | err = json.Unmarshal(body, &res) 98 | if err != nil { 99 | return nil, err 100 | } 101 | e.extensionID = httpRes.Header.Get(extensionIdentiferHeader) 102 | print(e.extensionID) 103 | return &res, nil 104 | } 105 | 106 | // NextEvent blocks while long polling for the next lambda invoke or shutdown 107 | func (e *Client) NextEvent(ctx context.Context) (*NextEventResponse, error) { 108 | 109 | const action = "/event/next" 110 | url := e.baseURL + action 111 | 112 | httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil) 113 | if err != nil { 114 | return nil, err 115 | } 116 | httpReq.Header.Set(extensionIdentiferHeader, e.extensionID) 117 | httpRes, err := e.httpClient.Do(httpReq) 118 | if err != nil { 119 | return nil, err 120 | } 121 | if httpRes.StatusCode != 200 { 122 | return nil, fmt.Errorf("request failed with status %s", httpRes.Status) 123 | } 124 | defer httpRes.Body.Close() 125 | body, err := io.ReadAll(httpRes.Body) 126 | if err != nil { 127 | return nil, err 128 | } 129 | res := NextEventResponse{} 130 | err = json.Unmarshal(body, &res) 131 | if err != nil { 132 | return nil, err 133 | } 134 | return &res, nil 135 | } 136 | -------------------------------------------------------------------------------- /internal/proxy/cache.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxy 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "encoding/hex" 10 | "fmt" 11 | "io" 12 | "io/ioutil" 13 | "net/http" 14 | "strings" 15 | 16 | "github.com/hashicorp/go-hclog" 17 | "github.com/hashicorp/vault-lambda-extension/internal/config" 18 | "github.com/hashicorp/vault/api" 19 | "github.com/hashicorp/vault/sdk/helper/cryptoutil" 20 | "github.com/hashicorp/vault/sdk/helper/locksutil" 21 | "github.com/hashicorp/vault/sdk/helper/strutil" 22 | gocache "github.com/patrickmn/go-cache" 23 | ) 24 | 25 | const ( 26 | // Return a cached response if it exists, otherwise fall back to Vault and 27 | // cache the response 28 | headerOptionCacheable = "cache" 29 | 30 | // Send the request to Vault and cache the response 31 | headerOptionRecache = "recache" 32 | 33 | // Ignore the cache and send the request to Vault, do not cache the response 34 | headerOptionNocache = "nocache" 35 | ) 36 | 37 | type Cache struct { 38 | data *gocache.Cache 39 | 40 | // defaultOn means caching is enabled for all requests without the need for 41 | // explicitly setting a caching header 42 | defaultOn bool 43 | 44 | // requestLocks is used during cache lookup to ensure that identical 45 | // requests made in parallel do not all hit vault 46 | requestLocks []*locksutil.LockEntry 47 | } 48 | 49 | type CacheKey struct { 50 | Token string 51 | Request *http.Request 52 | RequestBody []byte 53 | } 54 | 55 | type CacheData struct { 56 | StatusCode int 57 | Header http.Header 58 | Body []byte 59 | } 60 | 61 | type CacheOptions struct { 62 | cacheable bool 63 | recache bool 64 | nocache bool 65 | } 66 | 67 | func NewCache(cc config.CacheConfig) *Cache { 68 | return &Cache{ 69 | data: gocache.New(cc.TTL, cc.TTL), 70 | defaultOn: cc.DefaultEnabled, 71 | requestLocks: locksutil.CreateLocks(), 72 | } 73 | } 74 | 75 | // constructs the CacheKey for this request and token and returns the SHA256 76 | // hash 77 | func makeRequestHash(logger hclog.Logger, r *http.Request, token string) (string, error) { 78 | reqBody, err := io.ReadAll(r.Body) 79 | if err != nil { 80 | if r.Body != nil { 81 | if err := r.Body.Close(); err != nil { 82 | logger.Error("error closing request body", "error", err) 83 | } 84 | } 85 | return "", fmt.Errorf("failed to read request body: %w", err) 86 | } 87 | if r.Body != nil { 88 | if err := r.Body.Close(); err != nil { 89 | logger.Error("error closing request body", "error", err) 90 | } 91 | } 92 | r.Body = ioutil.NopCloser(bytes.NewReader(reqBody)) 93 | cacheKey := &CacheKey{ 94 | Token: token, 95 | Request: r, 96 | RequestBody: reqBody, 97 | } 98 | 99 | cacheKeyHash, err := computeRequestID(cacheKey) 100 | if err != nil { 101 | return "", fmt.Errorf("failed to compute request hash") 102 | } 103 | return cacheKeyHash, nil 104 | } 105 | 106 | // computeRequestID results in a value that uniquely identifies a request 107 | // received by the proxy. It does so by SHA256 hashing the serialized request 108 | // object containing the request path, query parameters and body parameters. 109 | func computeRequestID(key *CacheKey) (string, error) { 110 | var b bytes.Buffer 111 | 112 | if key == nil || key.Request == nil { 113 | return "", fmt.Errorf("cache key is nil") 114 | } 115 | 116 | cloned := key.Request.Clone(context.Background()) 117 | cloned.Header.Del(api.HeaderIndex) 118 | cloned.Header.Del(api.HeaderForward) 119 | cloned.Header.Del(api.HeaderInconsistent) 120 | cloned.Header.Del(VaultCacheControlHeaderName) 121 | // remove standard distributed tracing headers (https://www.w3.org/TR/trace-context/), 122 | // otherwise instrumented requests will always be unique 123 | cloned.Header.Del("Traceparent") 124 | cloned.Header.Del("Tracestate") 125 | // Serialize the request 126 | if err := cloned.Write(&b); err != nil { 127 | return "", fmt.Errorf("failed to serialize request: %v", err) 128 | } 129 | 130 | // Reset the request body after it has been closed by Write 131 | key.Request.Body = ioutil.NopCloser(bytes.NewReader(key.RequestBody)) 132 | 133 | // Append key.Token into the byte slice. Just in case the token was only 134 | // passed directly in CacheKey.Token, and not in a header. 135 | if _, err := b.Write([]byte(key.Token)); err != nil { 136 | return "", fmt.Errorf("failed to write token to hash input: %w", err) 137 | } 138 | 139 | return hex.EncodeToString(cryptoutil.Blake2b256Hash(b.String())), nil 140 | } 141 | 142 | func (c *Cache) Set(keyStr string, data *CacheData) { 143 | c.data.Set(keyStr, data, gocache.DefaultExpiration) 144 | } 145 | 146 | func (c *Cache) Get(keyStr string) (data *CacheData, err error) { 147 | dataRaw, found := c.data.Get(keyStr) 148 | if found && dataRaw != nil { 149 | var ok bool 150 | data, ok = dataRaw.(*CacheData) 151 | if !ok { 152 | return nil, fmt.Errorf("failed to convert cache item to CacheData for key %v", keyStr) 153 | } 154 | } 155 | 156 | return data, nil 157 | } 158 | 159 | func (c *Cache) Remove(keyStr string) { 160 | c.data.Delete(keyStr) 161 | } 162 | 163 | func setupCache(cacheConfig config.CacheConfig) *Cache { 164 | if cacheConfig.TTL <= 0 { 165 | return nil 166 | } 167 | return NewCache(cacheConfig) 168 | } 169 | 170 | func parseCacheOptions(cacheControlHeaders []string) *CacheOptions { 171 | values := []string{} 172 | for _, header := range cacheControlHeaders { 173 | values = append(values, strings.Split(header, ",")...) 174 | } 175 | options := &CacheOptions{ 176 | cacheable: strutil.StrListContains(values, headerOptionCacheable), 177 | recache: strutil.StrListContains(values, headerOptionRecache), 178 | nocache: strutil.StrListContains(values, headerOptionNocache), 179 | } 180 | 181 | return options 182 | } 183 | 184 | func shallFetchCache(r *http.Request, cache *Cache) bool { 185 | if cache == nil { 186 | return false 187 | } 188 | options := parseCacheOptions(r.Header.Values(VaultCacheControlHeaderName)) 189 | cacheable := (cache.defaultOn || options.cacheable) && !options.recache && !options.nocache 190 | return r.Method == http.MethodGet && cacheable 191 | } 192 | 193 | func shallRefreshCache(r *http.Request, cache *Cache) bool { 194 | if cache == nil { 195 | return false 196 | } 197 | options := parseCacheOptions(r.Header.Values(VaultCacheControlHeaderName)) 198 | cacheable := (cache.defaultOn || options.cacheable || options.recache) && !options.nocache 199 | return r.Method == http.MethodGet && cacheable 200 | } 201 | 202 | func fetchFromCache(w http.ResponseWriter, data *CacheData) { 203 | copyHeaders(w.Header(), data.Header) 204 | w.WriteHeader(data.StatusCode) 205 | w.Write(data.Body) 206 | } 207 | 208 | func retrieveData(resp *http.Response, body []byte) *CacheData { 209 | return &CacheData{ 210 | StatusCode: resp.StatusCode, 211 | Header: resp.Header, 212 | Body: body, 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /internal/proxy/cache_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxy 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "io/ioutil" 10 | "math/rand" 11 | "net/http" 12 | "net/http/httptest" 13 | "net/url" 14 | "reflect" 15 | "strings" 16 | "testing" 17 | "time" 18 | 19 | "github.com/hashicorp/go-hclog" 20 | "github.com/hashicorp/vault-lambda-extension/internal/config" 21 | "github.com/hashicorp/vault/api" 22 | "github.com/hashicorp/vault/sdk/helper/consts" 23 | "github.com/stretchr/testify/assert" 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | func TestCache_computeRequestID(t *testing.T) { 28 | tests := []struct { 29 | name string 30 | req *CacheKey 31 | want string 32 | wantErr bool 33 | }{ 34 | { 35 | "basic", 36 | &CacheKey{ 37 | Request: &http.Request{ 38 | URL: &url.URL{ 39 | Path: "test", 40 | }, 41 | }, 42 | }, 43 | "7b5db388f211fd9edca8c6c254831fb01ad4e6fe624dbb62711f256b5e803717", 44 | false, 45 | }, 46 | { 47 | "ignore consistency headers", 48 | &CacheKey{ 49 | Request: &http.Request{ 50 | URL: &url.URL{ 51 | Path: "test", 52 | }, 53 | Header: http.Header{ 54 | api.HeaderIndex: []string{"foo"}, 55 | api.HeaderInconsistent: []string{"foo"}, 56 | api.HeaderForward: []string{"foo"}, 57 | }, 58 | }, 59 | }, 60 | "7b5db388f211fd9edca8c6c254831fb01ad4e6fe624dbb62711f256b5e803717", 61 | false, 62 | }, 63 | { 64 | "ignore distributed tracing headers", 65 | &CacheKey{ 66 | Request: &http.Request{ 67 | URL: &url.URL{ 68 | Path: "test", 69 | }, 70 | Header: http.Header{ 71 | "Traceparent": []string{"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}, 72 | "Tracestate": []string{"rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"}, 73 | }, 74 | }, 75 | }, 76 | "7b5db388f211fd9edca8c6c254831fb01ad4e6fe624dbb62711f256b5e803717", 77 | false, 78 | }, 79 | { 80 | "nil CacheKey", 81 | nil, 82 | "", 83 | true, 84 | }, 85 | { 86 | "empty CacheKey", 87 | &CacheKey{}, 88 | "", 89 | true, 90 | }, 91 | } 92 | for _, tt := range tests { 93 | t.Run(tt.name, func(t *testing.T) { 94 | got, err := computeRequestID(tt.req) 95 | if (err != nil) != tt.wantErr { 96 | t.Errorf("actual_error: %v, expected_error: %v", err, tt.wantErr) 97 | return 98 | } 99 | if !reflect.DeepEqual(got, string(tt.want)) { 100 | t.Errorf("bad: index id; actual: %q, expected: %q", got, string(tt.want)) 101 | } 102 | }) 103 | } 104 | } 105 | 106 | func TestCache_computeRequestID_moreTests(t *testing.T) { 107 | t.Run("multiple times", func(t *testing.T) { 108 | req := &CacheKey{ 109 | Request: &http.Request{ 110 | URL: &url.URL{ 111 | Path: "test", 112 | }, 113 | Header: http.Header{ 114 | api.HeaderIndex: []string{"foo"}, 115 | api.HeaderInconsistent: []string{"foo"}, 116 | api.HeaderForward: []string{"foo"}, 117 | }, 118 | }, 119 | } 120 | got, err := computeRequestID(req) 121 | require.NoError(t, err) 122 | got2, err := computeRequestID(req) 123 | require.NoError(t, err) 124 | assert.Equal(t, got, got2) 125 | }) 126 | 127 | t.Run("token header changes hash", func(t *testing.T) { 128 | cacheKey := CacheKey{ 129 | Token: "blue", 130 | Request: &http.Request{ 131 | URL: &url.URL{ 132 | Path: "test", 133 | }, 134 | Header: http.Header{ 135 | consts.AuthHeaderName: []string{"blue"}, 136 | }, 137 | }, 138 | RequestBody: nil, 139 | } 140 | cacheKeyHash, err := computeRequestID(&cacheKey) 141 | require.NoError(t, err) 142 | require.NotEmpty(t, cacheKeyHash) 143 | 144 | // Remove the token header 145 | cacheKey.Request.Header.Del(consts.AuthHeaderName) 146 | cacheKeyHash2, err := computeRequestID(&cacheKey) 147 | require.NoError(t, err) 148 | require.NotEmpty(t, cacheKeyHash2) 149 | 150 | assert.NotEqual(t, cacheKeyHash, cacheKeyHash2) 151 | }) 152 | 153 | t.Run("namespace header changes hash", func(t *testing.T) { 154 | cacheKey := CacheKey{ 155 | Token: "blue", 156 | Request: &http.Request{ 157 | URL: &url.URL{ 158 | Path: "test", 159 | }, 160 | Header: http.Header{ 161 | consts.AuthHeaderName: []string{"blue"}, 162 | consts.NamespaceHeaderName: []string{"namespaced"}, 163 | }, 164 | }, 165 | RequestBody: nil, 166 | } 167 | cacheKeyHash, err := computeRequestID(&cacheKey) 168 | require.NoError(t, err) 169 | require.NotEmpty(t, cacheKeyHash) 170 | 171 | // Remove the namespace header 172 | cacheKey.Request.Header.Del(consts.NamespaceHeaderName) 173 | cacheKeyHash2, err := computeRequestID(&cacheKey) 174 | require.NoError(t, err) 175 | require.NotEmpty(t, cacheKeyHash2) 176 | 177 | assert.NotEqual(t, cacheKeyHash, cacheKeyHash2) 178 | }) 179 | 180 | t.Run("cache header does not change hash", func(t *testing.T) { 181 | cacheKey := CacheKey{ 182 | Token: "blue", 183 | Request: &http.Request{ 184 | URL: &url.URL{ 185 | Path: "test", 186 | }, 187 | Header: http.Header{ 188 | consts.AuthHeaderName: []string{"blue"}, 189 | consts.NamespaceHeaderName: []string{"namespaced"}, 190 | VaultCacheControlHeaderName: []string{headerOptionCacheable}, 191 | }, 192 | }, 193 | RequestBody: nil, 194 | } 195 | cacheKeyHash, err := computeRequestID(&cacheKey) 196 | require.NoError(t, err) 197 | require.NotEmpty(t, cacheKeyHash) 198 | 199 | // Remove the cache header 200 | cacheKey.Request.Header.Del(VaultCacheControlHeaderName) 201 | cacheKeyHash2, err := computeRequestID(&cacheKey) 202 | require.NoError(t, err) 203 | require.NotEmpty(t, cacheKeyHash2) 204 | 205 | assert.Equal(t, cacheKeyHash, cacheKeyHash2) 206 | }) 207 | } 208 | 209 | func Test_makeRequestHash(t *testing.T) { 210 | req := &http.Request{ 211 | URL: &url.URL{ 212 | Path: "test", 213 | }, 214 | Body: ioutil.NopCloser(bytes.NewBufferString("Hello World")), 215 | Header: http.Header{ 216 | consts.AuthHeaderName: []string{"blue"}, 217 | }, 218 | } 219 | 220 | h, err := makeRequestHash(hclog.Default(), req, "blue") 221 | assert.NoError(t, err) 222 | assert.Equal(t, "b62adf8925f91450ee992596dd2fb38edb0d3270ed9edc23b98bf5f322e9ed9a", h) 223 | } 224 | 225 | func TestNewCache(t *testing.T) { 226 | cache := NewCache(config.CacheConfig{ 227 | TTL: 10 * time.Second, 228 | }) 229 | require.NotNilf(t, cache, `NewCache(%s) returns nil`, "10*time.Second") 230 | 231 | cacheEnabled := NewCache(config.CacheConfig{ 232 | TTL: 10 * time.Second, 233 | DefaultEnabled: true, 234 | }) 235 | require.NotNil(t, cacheEnabled) 236 | } 237 | 238 | func TestSetupCache(t *testing.T) { 239 | t.Run("Valid vault cache TTL shall set up and return cache successfully", func(t *testing.T) { 240 | ttlArray := []time.Duration{5 * time.Minute, 1 * time.Second} 241 | for _, ttl := range ttlArray { 242 | cache := setupCache(config.CacheConfig{TTL: ttl}) 243 | require.NotNilf(t, cache, `setupCache() returns nil with env variable: %s`, ttl) 244 | assert.False(t, cache.defaultOn) 245 | } 246 | }) 247 | 248 | t.Run("Invalid vault cache TTL shall fail to set up and return cache", func(t *testing.T) { 249 | ttlArray := []time.Duration{-2 * time.Minute, 0} 250 | for _, ttl := range ttlArray { 251 | cache := setupCache(config.CacheConfig{TTL: ttl}) 252 | require.Nil(t, cache, `setupCache() does not return nil with ttl: %s`, ttl) 253 | } 254 | }) 255 | 256 | t.Run("Valid vault default cache enabled shall set up and return cache successfully", func(t *testing.T) { 257 | cache := setupCache(config.CacheConfig{TTL: 5 * time.Minute, DefaultEnabled: true}) 258 | require.NotNil(t, cache) 259 | assert.True(t, cache.defaultOn) 260 | }) 261 | 262 | t.Run("False vault default cache enabled shall result in cache.enabled=false", func(t *testing.T) { 263 | cache := setupCache(config.CacheConfig{TTL: 5 * time.Minute, DefaultEnabled: false}) 264 | require.NotNil(t, cache) 265 | assert.False(t, cache.defaultOn) 266 | }) 267 | } 268 | 269 | func TestGetAfterSet(t *testing.T) { 270 | t.Run("normal", func(t *testing.T) { 271 | cacheData := &CacheData{ 272 | Header: nil, 273 | Body: []byte(fmt.Sprint(rand.Intn(100))), 274 | StatusCode: http.StatusOK, 275 | } 276 | cache := NewCache(config.CacheConfig{TTL: 10 * time.Second}) 277 | cacheKey := CacheKey{ 278 | Token: "blue", 279 | Request: &http.Request{ 280 | URL: &url.URL{ 281 | Path: "test", 282 | }, 283 | Header: http.Header{ 284 | consts.AuthHeaderName: []string{"rose"}, 285 | }, 286 | }, 287 | RequestBody: nil, 288 | } 289 | cacheKeyHash, err := computeRequestID(&cacheKey) 290 | require.NoError(t, err) 291 | require.NotEmpty(t, cacheKeyHash) 292 | cache.Set(cacheKeyHash, cacheData) 293 | 294 | cacheDataOut, err := cache.Get(cacheKeyHash) 295 | require.NoError(t, err) 296 | assert.Equal(t, cacheData, cacheDataOut, `cache.Get() result doesn't match what was set with key: %s`, cacheKey) 297 | }) 298 | 299 | t.Run("expired item not returned", func(t *testing.T) { 300 | cacheData := &CacheData{ 301 | Header: nil, 302 | Body: []byte(fmt.Sprint(rand.Intn(100))), 303 | StatusCode: http.StatusOK, 304 | } 305 | cache := NewCache(config.CacheConfig{TTL: 10 * time.Millisecond}) 306 | cacheKey := CacheKey{ 307 | Token: "blue", 308 | Request: &http.Request{ 309 | URL: &url.URL{ 310 | Path: "test", 311 | }, 312 | Header: http.Header{ 313 | consts.AuthHeaderName: []string{"rose"}, 314 | }, 315 | }, 316 | RequestBody: nil, 317 | } 318 | cacheKeyHash, err := computeRequestID(&cacheKey) 319 | require.NoError(t, err) 320 | require.NotEmpty(t, cacheKeyHash) 321 | cache.Set(cacheKeyHash, cacheData) 322 | 323 | time.Sleep(20 * time.Millisecond) 324 | cacheDataOut, err := cache.Get(cacheKeyHash) 325 | 326 | require.NoError(t, err) 327 | assert.Nil(t, cacheDataOut) 328 | }) 329 | 330 | t.Run("deleted item not returned", func(t *testing.T) { 331 | cache := NewCache(config.CacheConfig{TTL: 1 * time.Hour}) 332 | cacheData := &CacheData{ 333 | Header: nil, 334 | Body: []byte(fmt.Sprint(rand.Intn(100))), 335 | StatusCode: http.StatusOK, 336 | } 337 | cacheKey := "test-key" 338 | cache.Set(cacheKey, cacheData) 339 | 340 | time.Sleep(5 * time.Second) 341 | cacheDataOut, err := cache.Get(cacheKey) 342 | require.NoError(t, err) 343 | assert.Equal(t, cacheData, cacheDataOut) 344 | 345 | cache.Remove(cacheKey) 346 | cacheDataOut2, err := cache.Get(cacheKey) 347 | require.NoError(t, err) 348 | require.Nil(t, cacheDataOut2) 349 | }) 350 | } 351 | 352 | func TestShallFetchCache(t *testing.T) { 353 | tests := map[string]struct { 354 | cache *Cache 355 | cacheControl string 356 | method string 357 | expected bool 358 | }{ 359 | "Shall fetch from cache when cache-control header is 'cache'": { 360 | cache: NewCache(config.CacheConfig{TTL: 10 * time.Second}), 361 | cacheControl: headerOptionCacheable, 362 | method: http.MethodGet, 363 | expected: true, 364 | }, 365 | "Shall not fetch from cache when cache is nil": { 366 | cache: nil, 367 | cacheControl: headerOptionCacheable, 368 | method: http.MethodGet, 369 | expected: false, 370 | }, 371 | "Shall not fetch from cache when cache-control header is incorrect": { 372 | cache: NewCache(config.CacheConfig{TTL: 10 * time.Second}), 373 | cacheControl: "crash,cache.,cache=1,cache=true", 374 | method: http.MethodGet, 375 | expected: false, 376 | }, 377 | "Shall not fetch from cache when cache-control is 'recache'": { 378 | cache: NewCache(config.CacheConfig{TTL: 10 * time.Second}), 379 | cacheControl: headerOptionRecache, 380 | method: http.MethodGet, 381 | expected: false, 382 | }, 383 | "Shall not fetch from cache when http method is not GET": { 384 | cache: NewCache(config.CacheConfig{TTL: 10 * time.Second}), 385 | cacheControl: headerOptionCacheable, 386 | method: http.MethodPost, 387 | expected: false, 388 | }, 389 | "Shall not fetch from cache when cache-control header is empty": { 390 | cache: NewCache(config.CacheConfig{TTL: 10 * time.Second}), 391 | cacheControl: "", 392 | method: http.MethodGet, 393 | expected: false, 394 | }, 395 | "No cache when cache-control header is nocache": { 396 | cache: NewCache(config.CacheConfig{TTL: 10 * time.Second}), 397 | cacheControl: headerOptionNocache, 398 | method: http.MethodGet, 399 | expected: false, 400 | }, 401 | "Shall fetch from cache when default enabled": { 402 | cache: NewCache(config.CacheConfig{TTL: 10 * time.Second, DefaultEnabled: true}), 403 | cacheControl: "", 404 | method: http.MethodGet, 405 | expected: true, 406 | }, 407 | "Shall not fetch from cache when default enabled and 'nocache' set": { 408 | cache: NewCache(config.CacheConfig{TTL: 10 * time.Second, DefaultEnabled: true}), 409 | cacheControl: headerOptionNocache, 410 | method: http.MethodGet, 411 | expected: false, 412 | }, 413 | "Shall not fetch from cache when default enabled and 'recache' set": { 414 | cache: NewCache(config.CacheConfig{TTL: 10 * time.Second, DefaultEnabled: true}), 415 | cacheControl: headerOptionRecache, 416 | method: http.MethodGet, 417 | expected: false, 418 | }, 419 | } 420 | for name, tc := range tests { 421 | t.Run(name, func(t *testing.T) { 422 | r := httptest.NewRequest(tc.method, "/v1/uuid/s1", nil) 423 | r.Header.Set(VaultCacheControlHeaderName, tc.cacheControl) 424 | shallFetch := shallFetchCache(r, tc.cache) 425 | assert.Equal(t, tc.expected, shallFetch) 426 | }) 427 | } 428 | } 429 | 430 | func TestShallFetchCache_multiple_headers(t *testing.T) { 431 | tests := map[string]struct { 432 | cacheControl []string 433 | expected bool 434 | }{ 435 | "No cache when 'nocache' set as second header": { 436 | cacheControl: []string{headerOptionCacheable, headerOptionNocache}, 437 | expected: false, 438 | }, 439 | "No cache when 'nocache' set as first header": { 440 | cacheControl: []string{headerOptionNocache, headerOptionCacheable}, 441 | expected: false, 442 | }, 443 | "Cache when repeated": { 444 | cacheControl: []string{headerOptionCacheable, headerOptionCacheable}, 445 | expected: true, 446 | }, 447 | "Cache when good and bad headers": { 448 | cacheControl: []string{"nope", headerOptionCacheable}, 449 | expected: true, 450 | }, 451 | } 452 | for name, tc := range tests { 453 | t.Run(name, func(t *testing.T) { 454 | cache := NewCache(config.CacheConfig{TTL: 10 * time.Second}) 455 | r := httptest.NewRequest(http.MethodGet, "/v1/uuid/s1", nil) 456 | for _, h := range tc.cacheControl { 457 | r.Header.Add(VaultCacheControlHeaderName, h) 458 | } 459 | shallFetch := shallFetchCache(r, cache) 460 | assert.Equal(t, tc.expected, shallFetch) 461 | }) 462 | } 463 | } 464 | 465 | func TestShallRefreshCache(t *testing.T) { 466 | tests := map[string]struct { 467 | cache *Cache 468 | cacheControl string 469 | expected bool 470 | }{ 471 | "Shall refresh cache when cache-control header is 'cache'": { 472 | cache: NewCache(config.CacheConfig{TTL: 10 * time.Second}), 473 | cacheControl: headerOptionCacheable, 474 | expected: true, 475 | }, 476 | "Shall refresh cache when cache-control header is 'recache'": { 477 | cache: NewCache(config.CacheConfig{TTL: 10 * time.Second}), 478 | cacheControl: headerOptionRecache, 479 | expected: true, 480 | }, 481 | "Shall refresh cache when cache-control header is 'cache,recache'": { 482 | cache: NewCache(config.CacheConfig{TTL: 10 * time.Second}), 483 | cacheControl: strings.Join([]string{headerOptionCacheable, headerOptionRecache}, ","), 484 | expected: true, 485 | }, 486 | "Shall not refresh cache when cache is nil": { 487 | cache: nil, 488 | cacheControl: headerOptionCacheable, 489 | expected: false, 490 | }, 491 | "Shall not refresh cache when cache-control header is incorrect": { 492 | cache: NewCache(config.CacheConfig{TTL: 10 * time.Second}), 493 | cacheControl: "nope", 494 | expected: false, 495 | }, 496 | "Shall not refresh cache when cache-control header is empty": { 497 | cache: NewCache(config.CacheConfig{TTL: 10 * time.Second}), 498 | cacheControl: "", 499 | expected: false, 500 | }, 501 | "No cache when cache-control header is nocache": { 502 | cache: NewCache(config.CacheConfig{TTL: 10 * time.Second}), 503 | cacheControl: headerOptionNocache, 504 | expected: false, 505 | }, 506 | "Shall refresh cache when default enabled": { 507 | cache: NewCache(config.CacheConfig{TTL: 10 * time.Second, DefaultEnabled: true}), 508 | cacheControl: "", 509 | expected: true, 510 | }, 511 | "Shall not refresh cache when default enabled and 'nocache' set": { 512 | cache: NewCache(config.CacheConfig{TTL: 10 * time.Second, DefaultEnabled: true}), 513 | cacheControl: headerOptionNocache, 514 | expected: false, 515 | }, 516 | "Shall refresh cache when default enabled and 'recache' set": { 517 | cache: NewCache(config.CacheConfig{TTL: 10 * time.Second, DefaultEnabled: true}), 518 | cacheControl: headerOptionRecache, 519 | expected: true, 520 | }, 521 | } 522 | for name, tc := range tests { 523 | t.Run(name, func(t *testing.T) { 524 | r := httptest.NewRequest("GET", "/v1/uuid/s1", nil) 525 | r.Header.Set(VaultCacheControlHeaderName, tc.cacheControl) 526 | shallFetch := shallRefreshCache(r, tc.cache) 527 | assert.Equal(t, tc.expected, shallFetch) 528 | }) 529 | } 530 | } 531 | 532 | func TestShallRefreshCache_multiple_headers(t *testing.T) { 533 | tests := map[string]struct { 534 | cacheControl []string 535 | expected bool 536 | }{ 537 | "Shall refresh cache when cache-control headers are 'cache' and 'recache'": { 538 | cacheControl: []string{headerOptionCacheable, headerOptionRecache}, 539 | expected: true, 540 | }, 541 | "Shall not refresh cache when cache-control headers are 'nocache' and 'recache'": { 542 | cacheControl: []string{headerOptionNocache, headerOptionRecache}, 543 | expected: false, 544 | }, 545 | "Shall not refresh cache when cache-control headers are 'recache' and 'nocache'": { 546 | cacheControl: []string{headerOptionRecache, headerOptionNocache}, 547 | expected: false, 548 | }, 549 | "Shall refresh cache with good and bad headers": { 550 | cacheControl: []string{headerOptionRecache, "nope", headerOptionCacheable}, 551 | expected: true, 552 | }, 553 | } 554 | for name, tc := range tests { 555 | t.Run(name, func(t *testing.T) { 556 | cache := NewCache(config.CacheConfig{TTL: 10 * time.Second}) 557 | r := httptest.NewRequest("GET", "/v1/uuid/s1", nil) 558 | for _, h := range tc.cacheControl { 559 | r.Header.Add(VaultCacheControlHeaderName, h) 560 | } 561 | shallFetch := shallRefreshCache(r, cache) 562 | assert.Equal(t, tc.expected, shallFetch) 563 | }) 564 | } 565 | } 566 | 567 | func TestRetrieveData(t *testing.T) { 568 | r := httptest.NewRequest("GET", "/v1/uuid/s1", nil) 569 | r.Header.Set(VaultCacheControlHeaderName, headerOptionCacheable) 570 | statusCode := 200 571 | body := "Hello World" 572 | resp := &http.Response{ 573 | Status: "200 OK", 574 | StatusCode: statusCode, 575 | Proto: "HTTP/1.1", 576 | ProtoMajor: 1, 577 | ProtoMinor: 1, 578 | Body: ioutil.NopCloser(bytes.NewBufferString("Not Hello World")), 579 | ContentLength: int64(len(body)), 580 | Request: r, 581 | Header: make(http.Header), 582 | } 583 | cacheData := retrieveData(resp, []byte(body)) 584 | require.Truef(t, cacheData.StatusCode == statusCode && string(cacheData.Body) == body, `retrieveData() shall return the same body: %s`, body) 585 | } 586 | -------------------------------------------------------------------------------- /internal/proxy/proxy.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxy 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/url" 12 | 13 | "github.com/hashicorp/go-hclog" 14 | "github.com/hashicorp/vault-lambda-extension/internal/config" 15 | "github.com/hashicorp/vault-lambda-extension/internal/vault" 16 | "github.com/hashicorp/vault/sdk/helper/consts" 17 | "github.com/hashicorp/vault/sdk/helper/locksutil" 18 | ) 19 | 20 | const ( 21 | VaultCacheControlHeaderName = "X-Vault-Cache-Control" 22 | VaultTokenOptionsHeaderName = "X-Vault-Token-Options" 23 | headerOptionRevokeToken = "revoke" 24 | proxyUserAgent = "; requesting from proxy" 25 | ) 26 | 27 | // New returns an unstarted HTTP server with health and proxy handlers. 28 | func New(logger hclog.Logger, client *vault.Client, cacheConfig config.CacheConfig) *http.Server { 29 | cache := setupCache(cacheConfig) 30 | mux := http.ServeMux{} 31 | mux.HandleFunc("/", proxyHandler(logger, client, cache)) 32 | srv := http.Server{ 33 | Handler: &mux, 34 | } 35 | 36 | return &srv 37 | } 38 | 39 | // The proxyHandler borrows from the Send function in Vault Agent's proxy: 40 | // https://github.com/hashicorp/vault/blob/22b486b651b8956d32fb24e77cef4050df7094b6/command/agent/cache/api_proxy.go 41 | func proxyHandler(logger hclog.Logger, client *vault.Client, cache *Cache) func(http.ResponseWriter, *http.Request) { 42 | return func(w http.ResponseWriter, r *http.Request) { 43 | if shouldRevokeToken(r.Header) { 44 | client.RevokeToken() 45 | } 46 | 47 | token, err := client.Token(r.Context()) 48 | if err != nil { 49 | http.Error(w, fmt.Sprintf("failed to get valid Vault token: %s", err), http.StatusInternalServerError) 50 | return 51 | } 52 | 53 | logger.Debug(fmt.Sprintf("Proxying %s %s", r.Method, r.URL.Path)) 54 | fwReq, err := proxyRequest(r, client.VaultConfig.Address, token) 55 | if err != nil { 56 | http.Error(w, fmt.Sprintf("failed to generate proxy request: %s", err), http.StatusInternalServerError) 57 | return 58 | } 59 | 60 | // If this request could result in cache.Get() or cache.Set(), acquire a 61 | // lock for this request's cache key to prevent parallel identical 62 | // requests from hitting Vault before one response is cached. 63 | doCacheGet := shallFetchCache(fwReq, cache) 64 | doCacheSet := shallRefreshCache(fwReq, cache) 65 | cacheKeyHash := "" 66 | if doCacheGet || doCacheSet { 67 | // Construct the hash for this request to use as the cache key 68 | cacheKeyHash, err = makeRequestHash(logger, r, token) 69 | if err != nil { 70 | logger.Error("failed to compute request hash", "error", err) 71 | http.Error(w, "failed to read request", http.StatusInternalServerError) 72 | return 73 | } 74 | 75 | requestLock := locksutil.LockForKey(cache.requestLocks, cacheKeyHash) 76 | requestLock.Lock() 77 | defer requestLock.Unlock() 78 | } 79 | 80 | if doCacheGet { 81 | // Check the cache for this request 82 | data, err := cache.Get(cacheKeyHash) 83 | if err != nil { 84 | logger.Error("failed to fetch from cache", "error", err) 85 | } 86 | if data != nil { 87 | logger.Debug(fmt.Sprintf("Cache hit for: %s %s", r.Method, r.URL.Path)) 88 | fetchFromCache(w, data) 89 | return 90 | } 91 | } 92 | 93 | resp, err := client.VaultConfig.HttpClient.Do(fwReq) 94 | if err != nil { 95 | http.Error(w, fmt.Sprintf("failed to proxy request: %s", err), http.StatusBadGateway) 96 | return 97 | } 98 | 99 | defer resp.Body.Close() 100 | 101 | // Save the response body 102 | var buf bytes.Buffer 103 | _, err = io.Copy(&buf, resp.Body) 104 | if err != nil { 105 | http.Error(w, fmt.Sprintf("failed to read response body: %s", err), http.StatusInternalServerError) 106 | return 107 | } 108 | respBody := buf.Bytes() 109 | 110 | if doCacheSet && resp.StatusCode < 300 { 111 | cache.Set(cacheKeyHash, retrieveData(resp, respBody)) 112 | logger.Debug(fmt.Sprintf("Refreshed cache for: %s %s", r.Method, r.URL.Path)) 113 | } 114 | 115 | copyHeaders(w.Header(), resp.Header) 116 | w.WriteHeader(resp.StatusCode) 117 | 118 | _, err = w.Write(respBody) 119 | if err != nil { 120 | http.Error(w, fmt.Sprintf("failed to write response back to requester: %s", err), http.StatusInternalServerError) 121 | return 122 | } 123 | 124 | logger.Debug(fmt.Sprintf("Successfully proxied %s %s", r.Method, r.URL.Path)) 125 | } 126 | } 127 | 128 | func proxyRequest(r *http.Request, vaultAddress string, token string) (*http.Request, error) { 129 | // http.Transport will transparently request gzip and decompress the response, but only if 130 | // the client doesn't manually set the header. Removing any Accept-Encoding header allows the 131 | // transparent compression to occur. 132 | r.Header.Del("Accept-Encoding") 133 | 134 | vault, err := url.Parse(vaultAddress) 135 | if err != nil { 136 | return nil, err 137 | } 138 | upstream := *r.URL 139 | upstream.Scheme = vault.Scheme 140 | upstream.Host = vault.Host 141 | 142 | fwReq, err := http.NewRequestWithContext(r.Context(), r.Method, upstream.String(), r.Body) 143 | if err != nil { 144 | return nil, err 145 | } 146 | fwReq.Header = r.Header 147 | fwReq.Header.Add(consts.AuthHeaderName, token) 148 | 149 | // add user agent header 150 | ua := config.GetUserAgentBase(config.ExtensionName, config.ExtensionVersion) 151 | fwReq.Header.Set("User-Agent", ua+proxyUserAgent) 152 | 153 | return fwReq, nil 154 | } 155 | 156 | func copyHeaders(dst, src http.Header) { 157 | for k, vs := range src { 158 | for _, v := range vs { 159 | dst.Add(k, v) 160 | } 161 | } 162 | } 163 | 164 | func shouldRevokeToken(headers http.Header) bool { 165 | return headers.Get(VaultTokenOptionsHeaderName) == headerOptionRevokeToken 166 | } 167 | -------------------------------------------------------------------------------- /internal/proxy/proxy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxy 5 | 6 | import ( 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "net" 12 | "net/http" 13 | "net/http/httptest" 14 | "strings" 15 | "testing" 16 | 17 | "github.com/aws/aws-sdk-go/aws/session" 18 | "github.com/hashicorp/go-hclog" 19 | "github.com/hashicorp/vault-lambda-extension/internal/config" 20 | "github.com/hashicorp/vault-lambda-extension/internal/ststest" 21 | "github.com/hashicorp/vault-lambda-extension/internal/vault" 22 | "github.com/hashicorp/vault/api" 23 | "github.com/stretchr/testify/require" 24 | ) 25 | 26 | type vaultResponse struct { 27 | secret *api.Secret 28 | err error 29 | code int 30 | } 31 | 32 | var ( 33 | vaultRequests []*http.Request 34 | fakeVaultResponse vaultResponse 35 | vaultResponseFooBar = vaultResponse{ 36 | secret: &api.Secret{ 37 | Data: map[string]interface{}{ 38 | "foo": "bar", 39 | }, 40 | }, 41 | } 42 | vaultResponse403 = vaultResponse{ 43 | err: errors.New("forbidden"), 44 | code: http.StatusForbidden, 45 | } 46 | vaultResponse500 = vaultResponse{ 47 | err: errors.New("internal server error"), 48 | code: http.StatusInternalServerError, 49 | } 50 | vaultResponse502 = vaultResponse{ 51 | err: errors.New("bad gateway"), 52 | code: http.StatusBadGateway, 53 | } 54 | vaultLoginResponse = &api.Secret{ 55 | Auth: &api.SecretAuth{ 56 | LeaseDuration: 3600, 57 | ClientToken: "foo", 58 | Renewable: true, 59 | }, 60 | } 61 | ) 62 | 63 | func TestProxy(t *testing.T) { 64 | fakeVault := fakeVault() 65 | defer fakeVault.Close() 66 | ses := session.Must(session.NewSession()) 67 | sts := ststest.FakeSTS(ses) 68 | defer sts.Close() 69 | proxyAddr, close := startProxy(t, fakeVault.URL, ses) 70 | defer close() 71 | 72 | t.Run("happy path bare http client", func(t *testing.T) { 73 | // reset request array 74 | vaultRequests = []*http.Request{} 75 | fakeVaultResponse = vaultResponseFooBar 76 | resp, err := http.Get(fmt.Sprintf("http://%s/v1/secret/data/foo", proxyAddr)) 77 | 78 | // the stored request should be the one _from the proxy_ since it's stored by 79 | // the (fake) vault. 80 | require.Contains(t, vaultRequests[1].Header.Get("User-Agent"), proxyUserAgent) 81 | require.NoError(t, err) 82 | require.Equal(t, http.StatusOK, resp.StatusCode) 83 | defer resp.Body.Close() 84 | body, err := io.ReadAll(resp.Body) 85 | require.NoError(t, err) 86 | var secret api.Secret 87 | require.NoError(t, json.Unmarshal(body, &secret), string(body)) 88 | require.Equal(t, "bar", secret.Data["foo"]) 89 | }) 90 | 91 | t.Run("happy path with vault client", func(t *testing.T) { 92 | vaultRequests = []*http.Request{} 93 | fakeVaultResponse = vaultResponseFooBar 94 | proxyVaultClient, err := api.NewClient(&api.Config{ 95 | Address: "http://" + proxyAddr, 96 | }) 97 | require.NoError(t, err) 98 | resp, err := proxyVaultClient.Logical().Read("secret/data/foo") 99 | 100 | require.Contains(t, vaultRequests[0].Header.Get("User-Agent"), proxyUserAgent) 101 | require.NoError(t, err) 102 | require.Equal(t, "bar", resp.Data["foo"]) 103 | }) 104 | 105 | t.Run("vault error codes should return unmodified", func(t *testing.T) { 106 | for _, tc := range []vaultResponse{vaultResponse403, vaultResponse500, vaultResponse502} { 107 | fakeVaultResponse = tc 108 | resp, err := http.Get(fmt.Sprintf("http://%s/v1/secret/data/foo", proxyAddr)) 109 | require.NoError(t, err) 110 | require.Equal(t, tc.code, resp.StatusCode) 111 | } 112 | }) 113 | 114 | t.Run("failed upstream request should give 502", func(t *testing.T) { 115 | 116 | fakeVaultResponse = vaultResponseFooBar 117 | resp, err := http.Get(fmt.Sprintf("http://%s/FailedTransport", proxyAddr)) 118 | require.NoError(t, err) 119 | require.Equal(t, http.StatusBadGateway, resp.StatusCode) 120 | }) 121 | 122 | t.Run("performs login when given token revoke header", func(t *testing.T) { 123 | fakeVaultResponse = vaultResponseFooBar 124 | // ensure the proxy has already logged in, ignore response, inspect next response 125 | _, err := http.Get(fmt.Sprintf("http://%s/v1/secret/data/foo", proxyAddr)) 126 | require.NoError(t, err) 127 | // reset request array to focus on next request 128 | vaultRequests = []*http.Request{} 129 | req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/v1/secret/data/foo", proxyAddr), nil) 130 | require.NoError(t, err) 131 | req.Header.Add(VaultTokenOptionsHeaderName, headerOptionRevokeToken) 132 | resp, err := http.DefaultClient.Do(req) 133 | 134 | // the stored request should be the one _from the proxy_ since it's stored by 135 | // the (fake) vault. 136 | // revoke should trigger another login call, and still get the secret 137 | require.Contains(t, vaultRequests[0].URL.Path, "login") 138 | require.NoError(t, err) 139 | require.Equal(t, http.StatusOK, resp.StatusCode) 140 | defer resp.Body.Close() 141 | body, err := io.ReadAll(resp.Body) 142 | require.NoError(t, err) 143 | var secret api.Secret 144 | require.NoError(t, json.Unmarshal(body, &secret), string(body)) 145 | require.Equal(t, "bar", secret.Data["foo"]) 146 | }) 147 | } 148 | 149 | func startProxy(t *testing.T, vaultAddress string, ses *session.Session) (string, func() error) { 150 | vaultConfig := api.DefaultConfig() 151 | require.NoError(t, vaultConfig.Error) 152 | vaultConfig.Address = vaultAddress 153 | client, err := vault.NewClient("", "", hclog.NewNullLogger(), vaultConfig, config.AuthConfig{}, ses) 154 | require.NoError(t, err) 155 | client.VaultConfig.Address = vaultAddress 156 | ln, err := net.Listen("tcp", "127.0.0.1:0") 157 | require.NoError(t, err) 158 | proxy := New(hclog.NewNullLogger(), client, config.CacheConfig{}) 159 | go func() { 160 | _ = proxy.Serve(ln) 161 | }() 162 | 163 | return ln.Addr().String(), proxy.Close 164 | } 165 | 166 | func fakeVault() *httptest.Server { 167 | vaultResponsePtr := &fakeVaultResponse 168 | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 169 | // after handling, save the request so we can inspect it in test cases 170 | defer func() { 171 | vaultRequests = append(vaultRequests, r) 172 | }() 173 | switch { 174 | case strings.Contains(r.URL.Path, "login"): 175 | b, err := json.Marshal(vaultLoginResponse) 176 | if err != nil { 177 | http.Error(w, "failed to marshal test response", 500) 178 | return 179 | } 180 | _, err = w.Write(b) 181 | if err != nil { 182 | http.Error(w, "failed to write response", 500) 183 | return 184 | } 185 | w.WriteHeader(200) 186 | case strings.Contains(r.URL.Path, "FailedTransport"): 187 | err := hijack(w) 188 | if err != nil { 189 | http.Error(w, err.Error(), 500) 190 | return 191 | } 192 | case vaultResponsePtr.err != nil: 193 | http.Error(w, vaultResponsePtr.err.Error(), vaultResponsePtr.code) 194 | default: 195 | bytes, err := json.Marshal(vaultResponsePtr.secret) 196 | if err != nil { 197 | http.Error(w, "failed to marshal JSON", 500) 198 | return 199 | } 200 | _, err = w.Write(bytes) 201 | if err != nil { 202 | http.Error(w, "failed to write response", 500) 203 | return 204 | } 205 | } 206 | })) 207 | } 208 | 209 | // hijack allows us to fail at the HTTP transport layer by writing an invalid 210 | // response. The proxy should return 502 when this happens. 211 | func hijack(w http.ResponseWriter) error { 212 | hijacker, ok := w.(http.Hijacker) 213 | if !ok { 214 | return errors.New("failed to hijack") 215 | } 216 | conn, buf, err := hijacker.Hijack() 217 | if err != nil { 218 | return err 219 | } 220 | defer conn.Close() 221 | 222 | _, err = buf.WriteString("Invalid HTTP response") 223 | if err != nil { 224 | return err 225 | } 226 | err = buf.Flush() 227 | if err != nil { 228 | return err 229 | } 230 | 231 | return nil 232 | } 233 | -------------------------------------------------------------------------------- /internal/runmode/runmode.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package runmode determines if the vault lambda extension should run in default mode, file mode, or proxy mode. 5 | // default mode: uses both file and proxy mode 6 | // file mode: writes secrets to disk 7 | // proxy mode: forwards requests to a Vault server 8 | package runmode 9 | 10 | import "strings" 11 | 12 | type Mode string 13 | 14 | var ( 15 | ModeDefault Mode = "default" 16 | ModeFile Mode = "file" 17 | ModeProxy Mode = "proxy" 18 | ) 19 | 20 | var modes = map[Mode]struct{}{ 21 | ModeFile: {}, 22 | ModeProxy: {}, 23 | ModeDefault: {}, 24 | } 25 | 26 | func ParseMode(rm string) Mode { 27 | _, ok := modes[Mode(strings.ToLower(rm))] 28 | if !ok { 29 | return ModeDefault 30 | } 31 | 32 | return Mode(rm) 33 | } 34 | 35 | func (m Mode) HasModeProxy() bool { 36 | return m == ModeDefault || m == ModeProxy 37 | } 38 | 39 | func (m Mode) HasModeFile() bool { 40 | return m == ModeDefault || m == ModeFile 41 | } 42 | -------------------------------------------------------------------------------- /internal/ststest/sts.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ststest 5 | 6 | import ( 7 | "net/http" 8 | "net/http/httptest" 9 | 10 | "github.com/aws/aws-sdk-go/aws/credentials" 11 | "github.com/aws/aws-sdk-go/aws/session" 12 | ) 13 | 14 | // FakeSTS creates a fake STS server, and configures the session passed in 15 | // to talk to that server. 16 | func FakeSTS(ses *session.Session) *httptest.Server { 17 | stsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | w.WriteHeader(200) 19 | })) 20 | 21 | ses.Config. 22 | WithEndpoint(stsServer.URL). 23 | WithRegion("us-east-1"). 24 | WithCredentials(credentials.NewStaticCredentialsFromCreds(credentials.Value{ 25 | ProviderName: session.EnvProviderName, 26 | AccessKeyID: "foo", 27 | SecretAccessKey: "foo", 28 | SessionToken: "foo", 29 | })) 30 | 31 | return stsServer 32 | } 33 | -------------------------------------------------------------------------------- /internal/vault/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package vault 5 | 6 | import ( 7 | "context" 8 | "encoding/base64" 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "os" 13 | "strings" 14 | "sync" 15 | "time" 16 | 17 | "github.com/aws/aws-sdk-go/aws" 18 | "github.com/aws/aws-sdk-go/aws/credentials" 19 | "github.com/aws/aws-sdk-go/aws/endpoints" 20 | 21 | "github.com/aws/aws-sdk-go/aws/session" 22 | "github.com/aws/aws-sdk-go/service/sts" 23 | "github.com/hashicorp/go-hclog" 24 | "github.com/hashicorp/vault/api" 25 | 26 | "github.com/hashicorp/vault-lambda-extension/internal/config" 27 | ) 28 | 29 | const ( 30 | tokenExpiryGracePeriodEnv = "VAULT_TOKEN_EXPIRY_GRACE_PERIOD" 31 | defaultTokenExpiryGracePeriod = 10 * time.Second 32 | ) 33 | 34 | // Client holds api.Client and handles state required to renew tokens and re-auth as required. 35 | type Client struct { 36 | Name string 37 | Version string 38 | 39 | mtx sync.Mutex 40 | 41 | VaultClient *api.Client 42 | VaultConfig *api.Config 43 | 44 | logger hclog.Logger 45 | stsSvc *sts.STS 46 | authConfig config.AuthConfig 47 | 48 | // Token refresh/renew data. 49 | tokenExpiryGracePeriod time.Duration 50 | tokenExpiry time.Time 51 | tokenTTL time.Duration 52 | tokenRenewable bool 53 | tokenRevoked bool 54 | } 55 | 56 | // NewClient uses the AWS IAM auth method configured in a Vault cluster to 57 | // authenticate the execution role and create a Vault API client. 58 | func NewClient(name, version string, logger hclog.Logger, vaultConfig *api.Config, authConfig config.AuthConfig, awsSes *session.Session) (*Client, error) { 59 | vaultClient, err := api.NewClient(vaultConfig) 60 | if err != nil { 61 | return nil, fmt.Errorf("error making extension: %w", err) 62 | } 63 | 64 | expiryGracePeriod, err := parseTokenExpiryGracePeriod() 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | client := &Client{ 70 | VaultClient: vaultClient, 71 | VaultConfig: vaultConfig, 72 | Name: name, 73 | Version: version, 74 | 75 | logger: logger, 76 | stsSvc: sts.New(awsSes), 77 | authConfig: authConfig, 78 | 79 | tokenExpiryGracePeriod: expiryGracePeriod, 80 | } 81 | 82 | return client, nil 83 | } 84 | 85 | // Token synchronously renews/re-auths as required and returns a Vault token. 86 | func (c *Client) Token(ctx context.Context) (string, error) { 87 | start := time.Now().Round(0) 88 | c.logger.Debug("fetching token") 89 | c.mtx.Lock() 90 | defer c.mtx.Unlock() 91 | 92 | if c.expired() || c.tokenRevoked { 93 | c.logger.Debug("authenticating to Vault") 94 | err := c.login(ctx) 95 | if err != nil { 96 | return "", err 97 | } 98 | } else if c.shouldRenew() { 99 | // Renew but don't retry or bail on errors, just best effort. 100 | c.logger.Debug("renewing Vault token") 101 | err := c.renew() 102 | if err != nil { 103 | c.logger.Error("failed to renew token but attempting to continue", "error", err) 104 | } 105 | } 106 | 107 | c.logger.Debug(fmt.Sprintf("fetched token in %v", time.Since(start))) 108 | return c.VaultClient.Token(), nil 109 | } 110 | 111 | // Mark token revoked 112 | func (c *Client) RevokeToken() { 113 | c.tokenRevoked = true 114 | } 115 | 116 | // login authenticates to Vault using IAM auth, and sets the client's token. 117 | func (c *Client) login(ctx context.Context) error { 118 | authConfig := config.AuthConfigFromEnv() 119 | roleToAssumeArn := authConfig.AssumedRoleArn 120 | 121 | stsSvc := c.stsSvc 122 | 123 | /* If passing in a role (through VAULT_ASSUMED_ROLE_ARN enviornment variable) 124 | to be assumed for Vault authentication, use it instead of the function execution role */ 125 | if roleToAssumeArn != "" { 126 | c.logger.Debug(fmt.Sprintf("Trying to assume role with arn of %s to authenticate with Vault", roleToAssumeArn)) 127 | sessionName := "vault_auth" 128 | 129 | result, err := c.stsSvc.AssumeRole(&sts.AssumeRoleInput{ 130 | RoleArn: &roleToAssumeArn, 131 | RoleSessionName: &sessionName, 132 | }) 133 | if err != nil { 134 | return fmt.Errorf("failed to assume role with arn of %s %w", roleToAssumeArn, err) 135 | } 136 | 137 | c.logger.Debug(fmt.Sprintf("Assumed role successfully with token expiration time: %s ", result.Credentials.Expiration.String())) 138 | 139 | var ses *session.Session 140 | if authConfig.STSEndpointRegion != "" { 141 | ses = session.Must(session.NewSession(&aws.Config{ 142 | Region: aws.String(authConfig.STSEndpointRegion), 143 | STSRegionalEndpoint: endpoints.RegionalSTSEndpoint, 144 | Credentials: credentials.NewStaticCredentials(*result.Credentials.AccessKeyId, *result.Credentials.SecretAccessKey, *result.Credentials.SessionToken), 145 | })) 146 | } else { 147 | ses = session.Must(session.NewSession(&aws.Config{ 148 | Credentials: credentials.NewStaticCredentials(*result.Credentials.AccessKeyId, *result.Credentials.SecretAccessKey, *result.Credentials.SessionToken), 149 | })) 150 | } 151 | 152 | stsSvc = sts.New(ses) 153 | } 154 | 155 | // ignore out 156 | req, _ := stsSvc.GetCallerIdentityRequest(&sts.GetCallerIdentityInput{}) 157 | req.SetContext(ctx) 158 | 159 | if c.authConfig.IAMServerID != "" { 160 | req.HTTPRequest.Header.Add("X-Vault-AWS-IAM-Server-ID", c.authConfig.IAMServerID) 161 | } 162 | 163 | if signErr := req.Sign(); signErr != nil { 164 | return signErr 165 | } 166 | 167 | headers, err := json.Marshal(req.HTTPRequest.Header) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | body, err := io.ReadAll(req.HTTPRequest.Body) 173 | if err != nil { 174 | return err 175 | } 176 | 177 | d := make(map[string]interface{}) 178 | d["iam_http_request_method"] = req.HTTPRequest.Method 179 | d["iam_request_url"] = base64.StdEncoding.EncodeToString([]byte(req.HTTPRequest.URL.String())) 180 | d["iam_request_headers"] = base64.StdEncoding.EncodeToString(headers) 181 | d["iam_request_body"] = base64.StdEncoding.EncodeToString(body) 182 | d["role"] = c.authConfig.Role 183 | 184 | secret, err := c.VaultClient.Logical().Write(fmt.Sprintf("auth/%s/login", c.authConfig.Provider), d) 185 | if err != nil { 186 | return err 187 | } 188 | if secret == nil { 189 | return fmt.Errorf("got no response from the %s authentication provider", c.authConfig.Provider) 190 | } 191 | 192 | token, err := secret.TokenID() 193 | if err != nil { 194 | return fmt.Errorf("error reading token: %s", err) 195 | } 196 | c.VaultClient.SetToken(token) 197 | 198 | return c.updateTokenMetadata(secret) 199 | } 200 | 201 | func (c *Client) renew() error { 202 | secret, err := c.VaultClient.Auth().Token().RenewSelf(int(c.tokenTTL.Seconds())) 203 | if err != nil { 204 | return err 205 | } 206 | 207 | return c.updateTokenMetadata(secret) 208 | } 209 | 210 | // Stores metadata about token lease that informs when to re-auth or renew. 211 | func (c *Client) updateTokenMetadata(secret *api.Secret) error { 212 | var err error 213 | c.tokenTTL, err = secret.TokenTTL() 214 | if err != nil { 215 | return err 216 | } 217 | 218 | c.tokenRevoked = false 219 | c.tokenExpiry = time.Now().Round(0).Add(c.tokenTTL) 220 | c.tokenRenewable, err = secret.TokenIsRenewable() 221 | if err != nil { 222 | return err 223 | } 224 | 225 | return nil 226 | } 227 | 228 | // Returns true if current time is after tokenExpiry, or within 10s. 229 | func (c *Client) expired() bool { 230 | return time.Now().Round(0).Add(c.tokenExpiryGracePeriod).After(c.tokenExpiry) 231 | } 232 | 233 | // Returns true if tokenExpiry time is in less than 20% of tokenTTL. 234 | func (c *Client) shouldRenew() bool { 235 | remaining := time.Until(c.tokenExpiry) 236 | return c.tokenRenewable && remaining.Nanoseconds() < c.tokenTTL.Nanoseconds()/5 237 | } 238 | 239 | func parseTokenExpiryGracePeriod() (time.Duration, error) { 240 | var err error 241 | expiryGracePeriod := defaultTokenExpiryGracePeriod 242 | 243 | expiryGracePeriodString := strings.TrimSpace(os.Getenv(tokenExpiryGracePeriodEnv)) 244 | if expiryGracePeriodString != "" { 245 | expiryGracePeriod, err = time.ParseDuration(expiryGracePeriodString) 246 | if err != nil { 247 | return 0, fmt.Errorf("unable to parse %q environment variable as a valid duration: %w", tokenExpiryGracePeriodEnv, err) 248 | } 249 | } 250 | 251 | return expiryGracePeriod, nil 252 | } 253 | 254 | // UserAgentRequestCallback takes a function that returns a user agent string and will invoke that function to set 255 | // the user agent string on the request. 256 | func UserAgentRequestCallback(agentFunc func(request *api.Request) string) api.RequestCallback { 257 | return func(req *api.Request) { 258 | req.Headers.Set("User-Agent", agentFunc(req)) 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /internal/vault/client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package vault 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "errors" 10 | "net/http" 11 | "net/http/httptest" 12 | "os" 13 | "testing" 14 | "time" 15 | 16 | "github.com/aws/aws-sdk-go/aws/session" 17 | "github.com/aws/aws-sdk-go/service/sts" 18 | "github.com/hashicorp/go-hclog" 19 | "github.com/hashicorp/vault-lambda-extension/internal/config" 20 | "github.com/hashicorp/vault-lambda-extension/internal/ststest" 21 | "github.com/hashicorp/vault/api" 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | var ( 26 | vaultRequests []*http.Request 27 | secretFunc func() (*api.Secret, error) 28 | 29 | with1hLease = &api.Secret{ 30 | Auth: &api.SecretAuth{ 31 | LeaseDuration: 3600, 32 | ClientToken: "foo-1h-token", 33 | Renewable: true, 34 | }, 35 | } 36 | with10hLease = &api.Secret{ 37 | Auth: &api.SecretAuth{ 38 | LeaseDuration: 36000, 39 | ClientToken: "foo-10h-token", 40 | Renewable: true, 41 | }, 42 | } 43 | ) 44 | 45 | func TestTokenRenewal(t *testing.T) { 46 | vault := fakeVault() 47 | defer vault.Close() 48 | ses := session.Must(session.NewSession()) 49 | stsServer := ststest.FakeSTS(ses) 50 | defer stsServer.Close() 51 | 52 | generateVaultClient := func() *api.Client { 53 | vaultClient, err := api.NewClient(&api.Config{ 54 | Address: vault.URL, 55 | }) 56 | require.NoError(t, err) 57 | return vaultClient 58 | } 59 | stsSvc := sts.New(ses) 60 | 61 | t.Run("TestExpired", func(t *testing.T) { 62 | now := time.Now() 63 | for _, tc := range []struct { 64 | name string 65 | expiry time.Time 66 | gracePeriod time.Duration 67 | expired bool 68 | }{ 69 | { 70 | name: "defaults to expired", 71 | expired: true, 72 | }, 73 | { 74 | name: "not expired", 75 | expiry: now.Add(time.Hour), 76 | gracePeriod: (10 * time.Second), 77 | expired: false, 78 | }, 79 | { 80 | name: "expired: falls inside grace period", 81 | expiry: now.Add(time.Hour), 82 | gracePeriod: time.Hour, 83 | expired: true, 84 | }, 85 | { 86 | name: "expired: expiry time in the past", 87 | expiry: now.Add(-time.Hour), 88 | gracePeriod: time.Second, 89 | expired: true, 90 | }, 91 | } { 92 | c := Client{ 93 | tokenExpiry: tc.expiry, 94 | tokenExpiryGracePeriod: tc.gracePeriod, 95 | } 96 | require.Equal(t, tc.expired, c.expired()) 97 | } 98 | }) 99 | 100 | t.Run("TestShouldRenew", func(t *testing.T) { 101 | now := time.Now() 102 | for _, tc := range []struct { 103 | name string 104 | expiry time.Time 105 | ttl time.Duration 106 | renewable bool 107 | expected bool 108 | }{ 109 | { 110 | name: "should renew", 111 | expiry: now.Add(time.Minute), 112 | ttl: time.Hour, 113 | renewable: true, 114 | expected: true, 115 | }, 116 | { 117 | name: "non-renewable token", 118 | expiry: now.Add(time.Minute), 119 | ttl: time.Hour, 120 | renewable: false, 121 | expected: false, 122 | }, 123 | { 124 | name: "lots of TTL still remaining", 125 | expiry: now.Add(time.Hour), 126 | ttl: time.Hour, 127 | renewable: true, 128 | expected: false, 129 | }, 130 | } { 131 | c := Client{ 132 | tokenExpiry: tc.expiry, 133 | tokenTTL: tc.ttl, 134 | tokenRenewable: tc.renewable, 135 | } 136 | require.Equal(t, tc.expected, c.shouldRenew()) 137 | } 138 | }) 139 | 140 | t.Run("TestToken_AlreadyLoggedIn_NoVaultCalls", func(t *testing.T) { 141 | vaultRequests = []*http.Request{} 142 | c := Client{ 143 | VaultClient: generateVaultClient(), 144 | logger: hclog.Default(), 145 | 146 | tokenExpiry: time.Now().Add(time.Hour), 147 | } 148 | secretFunc = nil 149 | _, err := c.Token(context.Background()) 150 | require.NoError(t, err) 151 | require.Equal(t, 0, len(vaultRequests)) 152 | }) 153 | 154 | t.Run("TestToken_MakesLoginCallIfExpired", func(t *testing.T) { 155 | vaultRequests = []*http.Request{} 156 | c := Client{ 157 | VaultClient: generateVaultClient(), 158 | logger: hclog.Default(), 159 | stsSvc: stsSvc, 160 | authConfig: config.AuthConfig{ 161 | Provider: "aws", 162 | }, 163 | } 164 | secretFunc = generateSecretFunc(t, []*api.Secret{ 165 | with1hLease, 166 | }) 167 | token, err := c.Token(context.Background()) 168 | require.NoError(t, err) 169 | require.Equal(t, 1, len(vaultRequests)) 170 | require.Equal(t, "/v1/auth/aws/login", vaultRequests[0].URL.Path) 171 | require.Equal(t, "foo-1h-token", token) 172 | require.Equal(t, time.Hour, c.tokenTTL) 173 | require.True(t, c.tokenRenewable) 174 | require.True(t, c.tokenExpiry.After(time.Now().Add(55*time.Minute))) 175 | }) 176 | 177 | t.Run("TestToken_MakesLoginCallIfRevoked", func(t *testing.T) { 178 | vaultRequests = []*http.Request{} 179 | c := Client{ 180 | VaultClient: generateVaultClient(), 181 | logger: hclog.Default(), 182 | stsSvc: stsSvc, 183 | authConfig: config.AuthConfig{ 184 | Provider: "aws", 185 | }, 186 | tokenExpiry: time.Now().Add(time.Hour), 187 | } 188 | c.RevokeToken() 189 | 190 | secretFunc = generateSecretFunc(t, []*api.Secret{ 191 | with1hLease, 192 | }) 193 | token, err := c.Token(context.Background()) 194 | require.NoError(t, err) 195 | require.Equal(t, 1, len(vaultRequests)) 196 | require.Equal(t, "/v1/auth/aws/login", vaultRequests[0].URL.Path) 197 | require.Equal(t, "foo-1h-token", token) 198 | require.Equal(t, time.Hour, c.tokenTTL) 199 | require.True(t, c.tokenRenewable) 200 | require.True(t, c.tokenExpiry.After(time.Now().Add(55*time.Minute))) 201 | }) 202 | 203 | t.Run("TestToken_MakesLoginCallIfExpired_PropagatesError", func(t *testing.T) { 204 | vaultRequests = []*http.Request{} 205 | c := Client{ 206 | VaultClient: generateVaultClient(), 207 | logger: hclog.Default(), 208 | stsSvc: stsSvc, 209 | authConfig: config.AuthConfig{ 210 | Provider: "aws", 211 | }, 212 | } 213 | secretFunc = func() (*api.Secret, error) { 214 | return nil, errors.New("failed login") 215 | } 216 | _, err := c.Token(context.Background()) 217 | require.Error(t, err) 218 | require.Equal(t, 1, len(vaultRequests)) 219 | require.Equal(t, "/v1/auth/aws/login", vaultRequests[0].URL.Path) 220 | }) 221 | 222 | t.Run("TestToken_MakesRenewCallAt90%TTL", func(t *testing.T) { 223 | vaultRequests = []*http.Request{} 224 | vaultClient := generateVaultClient() 225 | vaultClient.SetToken(t.Name()) 226 | c := Client{ 227 | VaultClient: vaultClient, 228 | logger: hclog.Default(), 229 | stsSvc: stsSvc, 230 | 231 | tokenRenewable: true, 232 | tokenExpiry: time.Now().Add(time.Hour), 233 | tokenTTL: 10 * time.Hour, 234 | } 235 | secretFunc = generateSecretFunc(t, []*api.Secret{ 236 | with10hLease, 237 | }) 238 | 239 | token, err := c.Token(context.Background()) 240 | require.NoError(t, err) 241 | 242 | // Token should not get updated by renew request. 243 | require.Equal(t, t.Name(), token) 244 | require.Equal(t, 1, len(vaultRequests)) 245 | require.Equal(t, "/v1/auth/token/renew-self", vaultRequests[0].URL.Path) 246 | 247 | // Token expiry should now be in another 10 hours 248 | require.True(t, c.tokenExpiry.After(time.Now().Add(9*time.Hour))) 249 | require.Equal(t, 10*time.Hour, c.tokenTTL) 250 | require.True(t, c.tokenRenewable) 251 | }) 252 | 253 | t.Run("TestToken_MakesRenewCallAt90%TTL_ErrorIsLoggedInsteadOfReturned", func(t *testing.T) { 254 | vaultRequests = []*http.Request{} 255 | vaultClient := generateVaultClient() 256 | vaultClient.SetToken(t.Name()) 257 | c := Client{ 258 | VaultClient: vaultClient, 259 | logger: hclog.Default(), 260 | stsSvc: stsSvc, 261 | 262 | tokenRenewable: true, 263 | tokenExpiry: time.Now().Add(time.Hour), 264 | tokenTTL: 10 * time.Hour, 265 | } 266 | secretFunc = func() (*api.Secret, error) { 267 | return nil, errors.New("failed renew") 268 | } 269 | 270 | token, err := c.Token(context.Background()) 271 | require.NoError(t, err) 272 | 273 | // Token should not get updated by failed renew request. 274 | require.Equal(t, t.Name(), token) 275 | require.Equal(t, 1, len(vaultRequests)) 276 | require.Equal(t, "/v1/auth/token/renew-self", vaultRequests[0].URL.Path) 277 | }) 278 | 279 | t.Run("TestToken_NoRenewRequestIfNotRenewable", func(t *testing.T) { 280 | vaultRequests = []*http.Request{} 281 | vaultClient := generateVaultClient() 282 | vaultClient.SetToken(t.Name()) 283 | c := Client{ 284 | VaultClient: vaultClient, 285 | logger: hclog.Default(), 286 | stsSvc: stsSvc, 287 | 288 | tokenRenewable: false, 289 | tokenExpiry: time.Now().Add(time.Hour), 290 | tokenTTL: 10 * time.Hour, 291 | } 292 | secretFunc = nil 293 | 294 | token, err := c.Token(context.Background()) 295 | require.NoError(t, err) 296 | 297 | // Token should not get updated by renew request. 298 | require.Equal(t, t.Name(), token) 299 | require.Equal(t, 0, len(vaultRequests)) 300 | }) 301 | } 302 | 303 | func TestParseTokenExpiryGracePeriod(t *testing.T) { 304 | for _, tc := range []struct { 305 | duration string 306 | expected time.Duration 307 | }{ 308 | {"", 10 * time.Second}, 309 | {"10000000000ns", 10 * time.Second}, 310 | {"1ns", time.Nanosecond}, 311 | {"2h", 2 * time.Hour}, 312 | } { 313 | require.NoError(t, os.Setenv(tokenExpiryGracePeriodEnv, tc.duration)) 314 | actual, err := parseTokenExpiryGracePeriod() 315 | require.NoError(t, err) 316 | require.Equal(t, tc.expected, actual) 317 | } 318 | 319 | // Error case. 320 | require.NoError(t, os.Setenv(tokenExpiryGracePeriodEnv, "foo")) 321 | _, err := parseTokenExpiryGracePeriod() 322 | require.Error(t, err) 323 | } 324 | 325 | const userAgent = "abcd" 326 | 327 | func TestUserAgentHeaderAddition(t *testing.T) { 328 | vault := fakeVault() 329 | generateVaultClient := func() *api.Client { 330 | vaultClient, err := api.NewClient(&api.Config{ 331 | Address: vault.URL, 332 | }) 333 | require.NoError(t, err) 334 | return vaultClient 335 | } 336 | 337 | t.Run("Ensure request contains header if decorator set", func(t *testing.T) { 338 | vaultRequests = []*http.Request{} 339 | vaultClient := generateVaultClient() 340 | vaultClient.SetToken(t.Name()) 341 | c := Client{ 342 | VaultClient: vaultClient, 343 | logger: hclog.Default(), 344 | //stsSvc: stsSvc, 345 | 346 | tokenRenewable: true, 347 | tokenExpiry: time.Now().Add(time.Hour), 348 | tokenTTL: 10 * time.Hour, 349 | } 350 | secretFunc = generateSecretFunc(t, []*api.Secret{ 351 | with10hLease, 352 | }) 353 | c.VaultClient = c.VaultClient.WithRequestCallbacks(UserAgentRequestCallback(fakeUserAgent)) 354 | 355 | _, err := c.Token(context.Background()) 356 | require.NoError(t, err) 357 | 358 | // validate request was set and the user agent is what we expect 359 | require.Equal(t, 1, len(vaultRequests)) 360 | require.Equal(t, userAgent, vaultRequests[0].Header.Get("User-Agent")) 361 | }) 362 | } 363 | 364 | func fakeUserAgent(_ *api.Request) string { 365 | return userAgent 366 | } 367 | 368 | func fakeVault() *httptest.Server { 369 | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 370 | defer func() { 371 | vaultRequests = append(vaultRequests, r) 372 | }() 373 | secret, err := secretFunc() 374 | if err != nil { 375 | http.Error(w, err.Error(), 400) 376 | return 377 | } 378 | bytes, err := json.Marshal(secret) 379 | if err != nil { 380 | http.Error(w, "failed to marshal JSON", 500) 381 | return 382 | } 383 | _, err = w.Write(bytes) 384 | if err != nil { 385 | http.Error(w, "failed to write response", 500) 386 | return 387 | } 388 | })) 389 | } 390 | 391 | func generateSecretFunc(t *testing.T, secrets []*api.Secret) func() (*api.Secret, error) { 392 | t.Helper() 393 | return func() (*api.Secret, error) { 394 | t.Helper() 395 | secret := secrets[0] 396 | secrets = secrets[1:] 397 | return secret, nil 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "net" 12 | "net/http" 13 | "os" 14 | "os/signal" 15 | "path" 16 | "sync" 17 | "syscall" 18 | "time" 19 | 20 | "github.com/aws/aws-sdk-go/aws" 21 | "github.com/aws/aws-sdk-go/aws/endpoints" 22 | "github.com/aws/aws-sdk-go/aws/session" 23 | "github.com/hashicorp/go-hclog" 24 | "github.com/hashicorp/vault/api" 25 | 26 | "github.com/hashicorp/vault-lambda-extension/internal/config" 27 | "github.com/hashicorp/vault-lambda-extension/internal/extension" 28 | "github.com/hashicorp/vault-lambda-extension/internal/proxy" 29 | "github.com/hashicorp/vault-lambda-extension/internal/runmode" 30 | "github.com/hashicorp/vault-lambda-extension/internal/vault" 31 | ) 32 | 33 | func main() { 34 | logger := hclog.New(&hclog.LoggerOptions{ 35 | Level: hclog.LevelFromString(os.Getenv(config.VaultLogLevel)), 36 | }) 37 | 38 | logger.Info(fmt.Sprintf("Starting Vault Lambda Extension %v", config.ExtensionVersion)) 39 | runMode := runmode.ModeDefault 40 | if runModeEnv := os.Getenv(config.VaultRunMode); runModeEnv != "" { 41 | runMode = runmode.ParseMode(runModeEnv) 42 | } 43 | 44 | h := newHandler(logger.Named(config.ExtensionName), runMode) 45 | if err := h.handle(); err != nil { 46 | logger.Error("Fatal error, exiting", "error", err) 47 | os.Exit(1) 48 | } 49 | } 50 | 51 | func newHandler(logger hclog.Logger, runMode runmode.Mode) *handler { 52 | return &handler{ 53 | logger: logger, 54 | runMode: runMode, 55 | } 56 | } 57 | 58 | type handler struct { 59 | logger hclog.Logger 60 | runMode runmode.Mode 61 | } 62 | 63 | func (h *handler) handle() error { 64 | ctx, cancel := context.WithCancel(context.Background()) 65 | defer cancel() 66 | 67 | var wg sync.WaitGroup 68 | cleanup, err := h.runExtension(ctx, &wg) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | shutdownChannel := make(chan struct{}) 74 | wg.Add(1) 75 | go func() { 76 | defer wg.Done() 77 | interruptChannel := make(chan os.Signal, 1) 78 | signal.Notify(interruptChannel, syscall.SIGTERM, syscall.SIGINT) 79 | select { 80 | case sig := <-interruptChannel: 81 | h.logger.Info("Received signal, exiting", "signal", sig) 82 | case <-shutdownChannel: 83 | h.logger.Info("Received shutdown event, exiting") 84 | } 85 | 86 | cancel() 87 | if err := cleanup(context.Background()); err != nil { 88 | // Error from closing listeners, or context timeout: 89 | h.logger.Error("HTTP server shutdown error", "error", err) 90 | } 91 | }() 92 | 93 | extensionClient := extension.NewClient(os.Getenv("AWS_LAMBDA_RUNTIME_API")) 94 | _, err = extensionClient.Register(ctx, config.ExtensionName) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | processEvents(ctx, h.logger, extensionClient) 100 | 101 | // Once processEvents returns, signal that it's time to shutdown. 102 | shutdownChannel <- struct{}{} 103 | 104 | // Ensure we wait for the HTTP server to gracefully shut down. 105 | wg.Wait() 106 | h.logger.Info("Graceful shutdown complete") 107 | 108 | return nil 109 | } 110 | 111 | func (h *handler) runExtension(ctx context.Context, wg *sync.WaitGroup) (func(context.Context) error, error) { 112 | start := time.Now() 113 | h.logger.Info("Initialising") 114 | 115 | authConfig := config.AuthConfigFromEnv() 116 | vaultConfig := api.DefaultConfig() 117 | if vaultConfig.Error != nil { 118 | return nil, fmt.Errorf("error making default vault config for extension: %w", vaultConfig.Error) 119 | } 120 | 121 | if authConfig.VaultAddress != "" { 122 | vaultConfig.Address = authConfig.VaultAddress 123 | } 124 | 125 | if vaultConfig.Address == "" || authConfig.Provider == "" || authConfig.Role == "" { 126 | return nil, errors.New("missing VLE_VAULT_ADDR, VAULT_ADDR, VAULT_AUTH_PROVIDER or VAULT_AUTH_ROLE environment variables") 127 | } 128 | 129 | var ses *session.Session 130 | if authConfig.STSEndpointRegion != "" { 131 | ses = session.Must(session.NewSession(&aws.Config{ 132 | Region: aws.String(authConfig.STSEndpointRegion), 133 | STSRegionalEndpoint: endpoints.RegionalSTSEndpoint, 134 | })) 135 | } else { 136 | ses = session.Must(session.NewSession()) 137 | } 138 | client, err := vault.NewClient(config.ExtensionName, config.ExtensionVersion, h.logger.Named("vault-client"), vaultConfig, authConfig, ses) 139 | if err != nil { 140 | return nil, fmt.Errorf("error getting client: %w", err) 141 | } else if client == nil { 142 | return nil, fmt.Errorf("nil client returned: %w", err) 143 | } 144 | 145 | var newState string 146 | // Leverage Vault helpers for eventual consistency on login 147 | client.VaultClient = client.VaultClient.WithResponseCallbacks(api.RecordState(&newState)) 148 | _, err = client.Token(ctx) 149 | if err != nil { 150 | return nil, fmt.Errorf("error logging in to Vault: %w", err) 151 | } 152 | 153 | uaFunc := func(request *api.Request) string { 154 | return config.GetUserAgentBase(config.ExtensionName, config.ExtensionVersion) + "; writing to temp file" 155 | } 156 | 157 | client.VaultClient = client.VaultClient.WithRequestCallbacks(api.RequireState(newState), vault.UserAgentRequestCallback(uaFunc)).WithResponseCallbacks() 158 | 159 | if h.runMode.HasModeFile() { 160 | if err := writePreconfiguredSecrets(h.logger, client.VaultClient); err != nil { 161 | return nil, err 162 | } 163 | } 164 | 165 | // clear out eventual consistency helpers 166 | client.VaultClient = client.VaultClient.WithRequestCallbacks().WithResponseCallbacks() 167 | 168 | cleanupFunc := func(context.Context) error { return nil } 169 | if h.runMode.HasModeProxy() { 170 | start := time.Now() 171 | h.logger.Debug("initialising proxy mode") 172 | ln, err := net.Listen("tcp", "127.0.0.1:8200") 173 | if err != nil { 174 | return nil, fmt.Errorf("failed to listen on port 8200: %w", err) 175 | } 176 | srv := proxy.New(h.logger.Named("proxy"), client, config.CacheConfigFromEnv()) 177 | wg.Add(1) 178 | go func() { 179 | defer wg.Done() 180 | h.logger.Info("Starting HTTP proxy server") 181 | err = srv.Serve(ln) 182 | if err != http.ErrServerClosed { 183 | h.logger.Error("HTTP server shutdown unexpectedly", "error", err) 184 | } 185 | }() 186 | cleanupFunc = func(ctx context.Context) error { 187 | return srv.Shutdown(ctx) 188 | } 189 | h.logger.Debug(fmt.Sprintf("proxy mode initialised in %v", time.Since(start))) 190 | } 191 | 192 | h.logger.Info(fmt.Sprintf("Initialised in %v", time.Since(start))) 193 | return cleanupFunc, nil 194 | } 195 | 196 | // writePreconfiguredSecrets writes secrets to disk. 197 | func writePreconfiguredSecrets(logger hclog.Logger, client *api.Client) error { 198 | start := time.Now() 199 | logger.Debug("writing secrets to disk") 200 | configuredSecrets, err := config.ParseConfiguredSecrets() 201 | if err != nil { 202 | return fmt.Errorf("failed to parse configured secrets to read: %w", err) 203 | } 204 | 205 | for _, s := range configuredSecrets { 206 | // Will block until shutdown event is received or cancelled via the context. 207 | secret, err := client.Logical().Read(s.VaultPath) 208 | if err != nil { 209 | return fmt.Errorf("error reading secret: %w", err) 210 | } 211 | 212 | content, err := json.MarshalIndent(secret, "", " ") 213 | if err != nil { 214 | return fmt.Errorf("unable to marshal json: %w", err) 215 | } 216 | 217 | dir := path.Dir(s.FilePath) 218 | if _, err = os.Stat(dir); os.IsNotExist(err) { 219 | if err := os.MkdirAll(dir, 0755); err != nil { 220 | return fmt.Errorf("failed to create directory %q for secret %s: %s", dir, s.Name(), err) 221 | } 222 | } 223 | 224 | if err := os.WriteFile(s.FilePath, content, 0644); err != nil { 225 | return fmt.Errorf("error writing file: %w", err) 226 | } 227 | } 228 | 229 | logger.Debug(fmt.Sprintf("wrote secrets to disk in %v", time.Since(start))) 230 | return nil 231 | } 232 | 233 | // processEvents polls the Lambda Extension API for events. Currently all this 234 | // does is signal readiness to the Lambda platform after each event, which is 235 | // required in the Extension API. 236 | // The first call to NextEvent signals completion of the extension 237 | // init phase. 238 | func processEvents(ctx context.Context, logger hclog.Logger, extensionClient *extension.Client) { 239 | for { 240 | select { 241 | case <-ctx.Done(): 242 | return 243 | default: 244 | logger.Info("Waiting for event...") 245 | res, err := extensionClient.NextEvent(ctx) 246 | if err != nil { 247 | logger.Error("Error receiving event", "error", err) 248 | return 249 | } 250 | logger.Info("Received event") 251 | // Exit if we receive a SHUTDOWN event 252 | if res.EventType == extension.Shutdown { 253 | return 254 | } 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /quick-start/README.md: -------------------------------------------------------------------------------- 1 | # Vault Lambda extension Quick Start 2 | 3 | This quick start folder has terraform and an example function for creating all the 4 | infrastructure you need to run a demo of the Vault Lambda extension. By default, 5 | the infrastructure is created in `us-east-1`. See [variables.tf](terraform/variables.tf) 6 | for the available variables, including region and instance types. 7 | 8 | The terraform will create: 9 | 10 | * An EC2 instance with a configured Vault server 11 | * A new SSH key pair used to SSH into the instance 12 | * IAM role for the Lambda to run as, configured for AWS IAM auth on Vault 13 | * An RDS database for which Vault can manage dynamic credentials 14 | * A Lambda function which requests database credentials from the extension and then uses them to list users on the database 15 | 16 | **NB: This demo will create real infrastructure in AWS with an associated 17 | cost. Make sure you tear down the infrastructure once you are finished with 18 | the demo.** 19 | 20 | **NB: This is not a production-ready deployment, and is for demonstration 21 | purposes only.** 22 | 23 | ## Prerequisites 24 | 25 | * `bash`, `zip` 26 | * Golang 27 | * Terraform 28 | * AWS account with access key ID and secret access key 29 | * AWS CLI v2 configured with the same account 30 | 31 | ## Usage 32 | 33 | ```bash 34 | ./build.sh 35 | cd terraform 36 | 37 | export AWS_ACCESS_KEY_ID = "" 38 | export AWS_SECRET_ACCESS_KEY = "" 39 | terraform init 40 | terraform apply 41 | 42 | # Then run the `aws lambda invoke` command from the terraform output 43 | 44 | # Remember to clean up the billed resources once you're finished 45 | terraform destroy 46 | ``` 47 | 48 | ### Deploying demo-function as an image 49 | 50 | This guide defaults to deploying the demo function in a zip format. 51 | 52 | Alternatively, to deploy using a container format Lambda, make the following 53 | modifications: 54 | 55 | * Create an ECR repository. You can use AWS CLI or console, or add this to 56 | the Terraform config: 57 | 58 | ```hcl 59 | resource "aws_ecr_repository" "demo-function" { 60 | name = "demo-function" 61 | } 62 | ``` 63 | 64 | **Note:** If you use Terraform to create the ECR repository, you will need 65 | to apply it before running `docker push` below, but creating the Lambda 66 | function will fail until you have pushed the image, so you may have to run the 67 | `terraform apply` step twice to resolve that partial failure. 68 | 69 | * Package the extension and function together into a Docker image. Use the 70 | `build-container.sh` script in this directory: 71 | 72 | ```bash 73 | ./build-container.sh 74 | ``` 75 | 76 | * Tag the image, and push it to your new ECR repository: 77 | 78 | ```bash 79 | export AWS_ACCOUNT="ACCOUNT HERE" 80 | export AWS_REGION="REGION HERE" 81 | docker tag demo-function:latest ${AWS_ACCOUNT?}.dkr.ecr.${AWS_REGION?}.amazonaws.com/demo-function:latest 82 | docker push ${AWS_ACCOUNT?}.dkr.ecr.${AWS_REGION?}.amazonaws.com/demo-function:latest 83 | ``` 84 | 85 | * Update `quick-start/terraform/lambda.tf` to use your image 86 | * Set `image_uri = ".dkr.ecr..amazonaws.com/demo-function:latest"` 87 | * Set `package_type = "Image"` 88 | * Unset `filename`, `handler`, `runtime`, and `layers` 89 | 90 | ## Credit 91 | 92 | Adapted from AWS KMS guides in the [vault-guides](https://github.com/hashicorp/vault-guides) repo. 93 | Specifically, mostly from [this guide](https://learn.hashicorp.com/tutorials/vault/agent-aws). 94 | -------------------------------------------------------------------------------- /quick-start/build-container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | # This script builds the demo-function and vault-lambda-extension into a Docker 6 | # container. Use if you would like to deploy the demo as an image instead of 7 | # a zip. See the quick-start readme for more details. 8 | 9 | set -euo pipefail 10 | 11 | # First, build vault-lambda-extension from source. 12 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 13 | pushd "${DIR}/../" 14 | GOOS=linux GOARCH=amd64 go build -ldflags '-s -w' -a -o quick-start/demo-function/pkg/extensions/vault-lambda-extension main.go 15 | popd 16 | 17 | # Build the container to be uploaded to Lambda, which will build demo-function 18 | # from source and copy vault-lambda-extension into the correct folder. 19 | docker build --platform linux/amd64 --file "${DIR}/demo-function/Dockerfile" --tag demo-function "${DIR}/demo-function/" 20 | -------------------------------------------------------------------------------- /quick-start/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | set -euo pipefail 7 | 8 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 9 | pushd "${DIR}/demo-function" 10 | 11 | echo "Removing old builds..." 12 | rm -f bin/main 2> /dev/null 13 | rm -f demo-function.zip 2> /dev/null 14 | 15 | echo "Build function..." 16 | GOOS=linux GOARCH=amd64 go build -ldflags '-s -w' -a -o bin/main main.go 17 | 18 | echo 19 | 20 | echo "Making new zip..." 21 | zip -j -D -r demo-function.zip bin/bootstrap bin/main 22 | 23 | popd # ${DIR} -------------------------------------------------------------------------------- /quick-start/demo-function/.gitignore: -------------------------------------------------------------------------------- 1 | main 2 | *.zip 3 | -------------------------------------------------------------------------------- /quick-start/demo-function/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # Based on vanilla Dockerfile to build a Golang image from: 5 | # https://docs.aws.amazon.com/lambda/latest/dg/go-image.html 6 | # The main modification is to copy the extension binary into the image. 7 | FROM golang:1.17.8 as build 8 | 9 | # Cache dependencies 10 | ADD go.mod go.sum /go/src/vault-lambda-extension/ 11 | WORKDIR /go/src/vault-lambda-extension/ 12 | RUN go mod download 13 | 14 | # Build 15 | ADD . . 16 | RUN go build -o /main 17 | 18 | # Copy artifacts to a clean image 19 | FROM public.ecr.aws/lambda/provided:al2 20 | COPY --from=build /main /main 21 | 22 | # Copy in the extension to a special location where the AWS Lambda Runtime API 23 | # knows to invoke it. 24 | COPY pkg/extensions/vault-lambda-extension /opt/extensions/vault-lambda-extension 25 | 26 | ENTRYPOINT [ "/main" ] 27 | -------------------------------------------------------------------------------- /quick-start/demo-function/README.md: -------------------------------------------------------------------------------- 1 | # demo-function 2 | 3 | This directory contains a demo function for the purposes of testing the 4 | vault-lambda-extension. It attempts to connect and query users from a database 5 | using two different sets of credentials; one set fetched from disk (which the 6 | extension wrote during initialization) and the other set from the proxy server 7 | which is queried during the function runtime. 8 | -------------------------------------------------------------------------------- /quick-start/demo-function/bin/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | LAMBDA_TASK_ROOT=/var/task 6 | TMPFILE=/tmp/data 7 | 8 | # Graceful Shutdown 9 | _term() { 10 | echo "[runtime] Received SIGTERM" 11 | # forward SIGTERM to child procs and exit 12 | kill -TERM "$PID" 2>/dev/null 13 | echo "[runtime] Exiting" 14 | exit 0 15 | } 16 | 17 | forward_sigterm_and_wait() { 18 | trap _term SIGTERM 19 | wait "$PID" 20 | trap - SIGTERM 21 | } 22 | 23 | # Initialization - load function handler 24 | echo "[runtime] handler in bootstrap: ${_HANDLER}" 25 | # source $LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1).sh" 26 | #.$LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1)" 27 | 28 | echo "[runtime] Initializing..." 29 | 30 | # Processing 31 | while true 32 | do 33 | echo "[runtime] Waiting for invocation..." 34 | 35 | HEADERS="$(mktemp)" 36 | 37 | # Get an event. The HTTP request will block until one is received 38 | curl -sS -LD "$HEADERS" -X GET "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next" > $TMPFILE & 39 | PID=$! 40 | forward_sigterm_and_wait 41 | 42 | EVENT_DATA=$(<$TMPFILE) 43 | 44 | echo "[runtime] Received invocation: $EVENT_DATA" 45 | 46 | # Extract request ID by scraping response headers received above 47 | REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2) 48 | 49 | echo "[runtime] Executing function: $_HANDLER" 50 | 51 | # Execute the handler function from the script 52 | RESPONSE=$($(echo ".$LAMBDA_TASK_ROOT/$_HANDLER" | cut -d. -f2) "$EVENT_DATA") 53 | 54 | echo "[runtime] Sending invocation response: $RESPONSE" 55 | 56 | # Send the response 57 | curl -sS -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response" -d "$RESPONSE" > $TMPFILE 58 | PID=$! 59 | forward_sigterm_and_wait 60 | 61 | STATUS_RESP=$(<$TMPFILE) 62 | 63 | echo "[runtime] Runtime API response: $STATUS_RESP" 64 | done 65 | -------------------------------------------------------------------------------- /quick-start/demo-function/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/vault-ext-proto 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/aws/aws-lambda-go v1.19.1 7 | github.com/hashicorp/vault/api v1.12.2 8 | github.com/lib/pq v1.8.0 9 | ) 10 | 11 | require ( 12 | github.com/cenkalti/backoff/v3 v3.0.0 // indirect 13 | github.com/go-jose/go-jose/v3 v3.0.4 // indirect 14 | github.com/hashicorp/errwrap v1.1.0 // indirect 15 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 16 | github.com/hashicorp/go-multierror v1.1.1 // indirect 17 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 18 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 19 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect 20 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 21 | github.com/hashicorp/go-sockaddr v1.0.2 // indirect 22 | github.com/hashicorp/hcl v1.0.0 // indirect 23 | github.com/mitchellh/go-homedir v1.1.0 // indirect 24 | github.com/mitchellh/mapstructure v1.5.0 // indirect 25 | github.com/ryanuber/go-glob v1.0.0 // indirect 26 | golang.org/x/crypto v0.36.0 // indirect 27 | golang.org/x/net v0.38.0 // indirect 28 | golang.org/x/text v0.23.0 // indirect 29 | golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /quick-start/demo-function/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 3 | github.com/aws/aws-lambda-go v1.19.1 h1:5iUHbIZ2sG6Yq/J1IN3sWm3+vAB1CWwhI21NffLNuNI= 4 | github.com/aws/aws-lambda-go v1.19.1/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= 5 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 6 | github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= 7 | github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= 8 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 9 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 14 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 15 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 16 | github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= 17 | github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= 18 | github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= 19 | github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 20 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 21 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 22 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 23 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 24 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 25 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 26 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 27 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 28 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 29 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 30 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 31 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 32 | github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= 33 | github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= 34 | github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= 35 | github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= 36 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= 37 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= 38 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= 39 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= 40 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= 41 | github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= 42 | github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= 43 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 44 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 45 | github.com/hashicorp/vault/api v1.12.2 h1:7YkCTE5Ni90TcmYHDBExdt4WGJxhpzaHqR6uGbQb/rE= 46 | github.com/hashicorp/vault/api v1.12.2/go.mod h1:LSGf1NGT1BnvFFnKVtnvcaLBM2Lz+gJdpL6HUYed8KE= 47 | github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= 48 | github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 49 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 50 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 51 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 52 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 53 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 54 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 55 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 56 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 57 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 58 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 59 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 60 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 61 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 62 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 63 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 64 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 65 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 66 | github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 67 | github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= 68 | github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 69 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 70 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 71 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 72 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 73 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 74 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 75 | github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 76 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 77 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 78 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 79 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 80 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 81 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 82 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 83 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 84 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 85 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 86 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 87 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 88 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 89 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 90 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 91 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 92 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 93 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 94 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 95 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 96 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 97 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 98 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 99 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 100 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 101 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 102 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 103 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 104 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 105 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 106 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 107 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 108 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 109 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 110 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 111 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 112 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 113 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 114 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 115 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 116 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 117 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 118 | golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= 119 | golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 120 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 121 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 122 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 123 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 124 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 125 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 126 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 127 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 128 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 129 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 130 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 131 | -------------------------------------------------------------------------------- /quick-start/demo-function/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "fmt" 10 | "log" 11 | "os" 12 | 13 | "github.com/aws/aws-lambda-go/lambda" 14 | "github.com/hashicorp/vault/api" 15 | 16 | "database/sql" 17 | 18 | _ "github.com/lib/pq" 19 | ) 20 | 21 | const functionName = "demo-function" 22 | 23 | // Payload captures the basic payload we're sending for demonstration 24 | // Ex: {"payload": "hello"} 25 | type Payload struct { 26 | Message string `json:"payload"` 27 | } 28 | 29 | // String prints the payload recieved 30 | func (m Payload) String() string { 31 | return m.Message 32 | } 33 | 34 | func handle(ctx context.Context, payload Payload) error { 35 | logger := log.New(os.Stderr, fmt.Sprintf("[%s] ", functionName), 0) 36 | err := handleRequest(ctx, payload, logger) 37 | if err != nil { 38 | logger.Println("Error handling request", err) 39 | } 40 | return err 41 | } 42 | 43 | // handleRequest reads credentials from /tmp and uses them to query the database 44 | // for users. The database is determined by the DATABASE_URL environment 45 | // variable, and the username and password are retrieved from the secret. 46 | func handleRequest(ctx context.Context, payload Payload, logger *log.Logger) error { 47 | logger.Println("Received:", payload.String()) 48 | logger.Println("Reading file /tmp/vault_secret.json") 49 | 50 | runMode := os.Getenv("VAULT_RUN_MODE") 51 | logger.Println("VAULT_RUN_MODE", runMode) 52 | if runMode == "" { 53 | runMode = "default" 54 | } 55 | 56 | if runMode == "default" || runMode == "file" { 57 | secretRaw, err := os.ReadFile("/tmp/vault_secret.json") 58 | if err != nil { 59 | return fmt.Errorf("error reading file: %w", err) 60 | } 61 | 62 | var secret api.Secret 63 | err = json.Unmarshal(secretRaw, &secret) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | logger.Println("Querying users using credentials from disk") 69 | err = readUsersFromDatabase(ctx, logger, &secret) 70 | if err != nil { 71 | logger.Println("Failed to read users from database", err) 72 | } 73 | } 74 | 75 | if runMode == "default" || runMode == "proxy" { 76 | proxyClient, err := api.NewClient(&api.Config{ 77 | Address: "http://127.0.0.1:8200", 78 | }) 79 | if err != nil { 80 | return err 81 | } 82 | proxySecret, err := proxyClient.Logical().Read(os.Getenv("VAULT_SECRET_PATH_DB")) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | logger.Println("Querying users using credentials from proxy") 88 | err = readUsersFromDatabase(ctx, logger, proxySecret) 89 | if err != nil { 90 | logger.Println("Failed to read users from database", err) 91 | } 92 | } 93 | return nil 94 | } 95 | 96 | func readUsersFromDatabase(ctx context.Context, logger *log.Logger, secret *api.Secret) error { 97 | connStr := fmt.Sprintf("postgres://%s:%s@%s/lambdadb?sslmode=disable", secret.Data["username"], secret.Data["password"], os.Getenv("DATABASE_URL")) 98 | db, err := sql.Open("postgres", connStr) 99 | if err != nil { 100 | return err 101 | } 102 | defer db.Close() 103 | 104 | var users []string 105 | rows, err := db.QueryContext(ctx, "SELECT usename FROM pg_catalog.pg_user") 106 | if err != nil { 107 | return err 108 | } 109 | defer rows.Close() 110 | for rows.Next() { 111 | var user string 112 | if err = rows.Scan(&user); err != nil { 113 | return err 114 | } 115 | users = append(users, user) 116 | } 117 | logger.Println("users: ") 118 | for i := range users { 119 | logger.Println(" ", users[i]) 120 | } 121 | 122 | return nil 123 | } 124 | 125 | func main() { 126 | lambda.Start(handle) 127 | } 128 | -------------------------------------------------------------------------------- /quick-start/terraform/.gitignore: -------------------------------------------------------------------------------- 1 | .terraform* 2 | terraform.tfvars 3 | private.key 4 | terraform.tfstate 5 | terraform.tfstate.backup -------------------------------------------------------------------------------- /quick-start/terraform/aws.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | //-------------------------------------------------------------------- 5 | // Providers 6 | 7 | provider "aws" { 8 | // Credentials set via env vars 9 | region = var.aws_region 10 | } 11 | 12 | //-------------------------------------------------------------------- 13 | // Data Sources 14 | 15 | data "aws_ami" "ubuntu" { 16 | most_recent = true 17 | 18 | filter { 19 | name = "name" 20 | values = ["ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-*"] 21 | } 22 | 23 | filter { 24 | name = "virtualization-type" 25 | values = ["hvm"] 26 | } 27 | 28 | owners = ["099720109477"] # Canonical 29 | } 30 | 31 | -------------------------------------------------------------------------------- /quick-start/terraform/iam.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | //-------------------------------------------------------------------- 5 | // Resources 6 | 7 | ## Vault Server IAM Config 8 | resource "aws_iam_instance_profile" "vault-server" { 9 | name = "${var.environment_name}-vault-server-instance-profile" 10 | role = aws_iam_role.vault-server.name 11 | } 12 | 13 | resource "aws_iam_role" "vault-server" { 14 | name = "${var.environment_name}-vault-server-role" 15 | assume_role_policy = data.aws_iam_policy_document.assume_role_ec2.json 16 | } 17 | 18 | resource "aws_iam_role_policy" "vault-server" { 19 | name = "${var.environment_name}-vault-server-role-policy" 20 | role = aws_iam_role.vault-server.id 21 | policy = data.aws_iam_policy_document.vault-server.json 22 | } 23 | 24 | # Vault Client (Lambda function) IAM Config 25 | resource "aws_iam_instance_profile" "lambda" { 26 | name = "${var.environment_name}-lambda-instance-profile" 27 | role = aws_iam_role.lambda.name 28 | } 29 | 30 | resource "aws_iam_role" "lambda" { 31 | name = "${var.environment_name}-lambda-role" 32 | assume_role_policy = var.assume_role ? data.aws_iam_policy_document.assume_role_lambda_plus_root[0].json : data.aws_iam_policy_document.assume_role_lambda.json 33 | } 34 | 35 | resource "aws_iam_role" "extra_role" { 36 | count = var.assume_role ? 1 : 0 37 | name = "${var.environment_name}-extra-role" 38 | assume_role_policy = data.aws_iam_policy_document.assume_role_lambda_plus_root[0].json 39 | } 40 | 41 | resource "aws_iam_role_policy" "lambda" { 42 | name = "${var.environment_name}-lambda-policy" 43 | role = aws_iam_role.lambda.id 44 | policy = var.assume_role ? data.aws_iam_policy_document.lambda_plus_assume_role[0].json : data.aws_iam_policy_document.lambda.json 45 | } 46 | 47 | resource "aws_iam_role_policy" "extra_role_policy" { 48 | count = var.assume_role ? 1 : 0 49 | name = "${var.environment_name}-extra-role-policy" 50 | role = aws_iam_role.extra_role[0].id 51 | policy = data.aws_iam_policy_document.lambda_plus_assume_role[0].json 52 | } 53 | 54 | //-------------------------------------------------------------------- 55 | // Data Sources 56 | 57 | data "aws_iam_policy_document" "assume_role_ec2" { 58 | statement { 59 | effect = "Allow" 60 | actions = ["sts:AssumeRole"] 61 | 62 | principals { 63 | type = "Service" 64 | identifiers = ["ec2.amazonaws.com"] 65 | } 66 | } 67 | } 68 | 69 | data "aws_iam_policy_document" "assume_role_lambda" { 70 | statement { 71 | effect = "Allow" 72 | actions = ["sts:AssumeRole"] 73 | 74 | principals { 75 | type = "Service" 76 | identifiers = ["lambda.amazonaws.com"] 77 | } 78 | } 79 | } 80 | 81 | data "aws_iam_policy_document" "assume_role_lambda_plus_root" { 82 | count = var.assume_role ? 1 : 0 83 | 84 | statement { 85 | effect = "Allow" 86 | actions = ["sts:AssumeRole"] 87 | 88 | principals { 89 | type = "Service" 90 | identifiers = ["lambda.amazonaws.com"] 91 | } 92 | 93 | principals { 94 | type = "AWS" 95 | identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"] 96 | } 97 | } 98 | } 99 | 100 | data "aws_iam_policy_document" "vault-server" { 101 | statement { 102 | sid = "ConsulAutoJoin" 103 | effect = "Allow" 104 | 105 | actions = ["ec2:DescribeInstances"] 106 | 107 | resources = ["*"] 108 | } 109 | 110 | statement { 111 | sid = "VaultAWSAuthMethod" 112 | effect = "Allow" 113 | actions = [ 114 | "ec2:DescribeInstances", 115 | "iam:GetInstanceProfile", 116 | "iam:GetUser", 117 | "iam:GetRole", 118 | ] 119 | resources = ["*"] 120 | } 121 | 122 | statement { 123 | sid = "VaultKMSUnseal" 124 | effect = "Allow" 125 | 126 | actions = [ 127 | "kms:Encrypt", 128 | "kms:Decrypt", 129 | "kms:DescribeKey", 130 | ] 131 | 132 | resources = ["*"] 133 | } 134 | } 135 | 136 | data "aws_iam_policy_document" "lambda" { 137 | statement { 138 | sid = "LambdaLogs" 139 | effect = "Allow" 140 | 141 | actions = [ 142 | "logs:CreateLogGroup", 143 | "logs:CreateLogStream", 144 | "logs:PutLogEvents" 145 | ] 146 | 147 | resources = ["*"] 148 | } 149 | } 150 | 151 | data "aws_iam_policy_document" "lambda_plus_assume_role" { 152 | count = var.assume_role ? 1 : 0 153 | 154 | statement { 155 | sid = "LambdaLogs" 156 | effect = "Allow" 157 | 158 | actions = [ 159 | "logs:CreateLogGroup", 160 | "logs:CreateLogStream", 161 | "logs:PutLogEvents" 162 | ] 163 | 164 | resources = ["*"] 165 | } 166 | 167 | statement { 168 | effect = "Allow" 169 | actions = ["sts:AssumeRole"] 170 | resources = ["*"] 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /quick-start/terraform/kms.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | //-------------------------------------------------------------------- 5 | // Resources 6 | 7 | resource "aws_kms_key" "vault" { 8 | description = "Vault unseal key" 9 | deletion_window_in_days = 7 10 | 11 | tags = { 12 | Name = "${var.environment_name}-vault-kms-unseal-key" 13 | } 14 | } 15 | 16 | resource "aws_kms_alias" "vault" { 17 | name = "alias/${var.environment_name}-vault-kms-unseal-key" 18 | target_key_id = aws_kms_key.vault.key_id 19 | } 20 | -------------------------------------------------------------------------------- /quick-start/terraform/lambda.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | resource "aws_lambda_function" "function" { 5 | function_name = "${var.environment_name}-function" 6 | description = "Demo Vault AWS Lambda extension" 7 | role = aws_iam_role.lambda.arn 8 | filename = "../demo-function/demo-function.zip" 9 | handler = "main" 10 | runtime = "provided.al2" 11 | architectures = ["x86_64"] 12 | layers = var.local_extension ? ["${aws_lambda_layer_version.vle[0].arn}"] : [ 13 | "arn:aws:lambda:${var.aws_region}:634166935893:layer:vault-lambda-extension:22" 14 | ] 15 | 16 | environment { 17 | variables = { 18 | VAULT_ADDR = "http://${aws_instance.vault-server.public_ip}:8200", 19 | VAULT_AUTH_ROLE = aws_iam_role.lambda.name, 20 | VAULT_AUTH_PROVIDER = "aws", 21 | VAULT_SECRET_PATH_DB = "database/creds/lambda-function", 22 | VAULT_SECRET_FILE_DB = "/tmp/vault_secret.json", 23 | VAULT_SECRET_PATH = "secret/myapp/config", 24 | VAULT_ASSUMED_ROLE_ARN = var.assume_role ? aws_iam_role.extra_role[0].arn : "", 25 | VAULT_RUN_MODE = "default", 26 | VAULT_LOG_LEVEL = "debug", 27 | DATABASE_URL = aws_db_instance.main.address 28 | } 29 | } 30 | } 31 | 32 | // if you have built a local version you want to use 33 | resource "aws_lambda_layer_version" "vle" { 34 | count = var.local_extension ? 1 : 0 35 | filename = "../../pkg/vault-lambda-extension.zip" 36 | layer_name = "vault-lambda-extension" 37 | compatible_architectures = ["x86_64"] 38 | } 39 | -------------------------------------------------------------------------------- /quick-start/terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | output "info" { 5 | value = < private.key" 11 | } 12 | 13 | provisioner "local-exec" { 14 | command = "chmod 600 private.key" 15 | } 16 | } 17 | 18 | resource "aws_key_pair" "main" { 19 | key_name = "vault-kms-unseal-${random_pet.env.id}" 20 | public_key = tls_private_key.main.public_key_openssh 21 | } 22 | 23 | resource "random_pet" "env" { 24 | length = 2 25 | separator = "-" 26 | } 27 | -------------------------------------------------------------------------------- /quick-start/terraform/templates/userdata-vault-server.tpl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | exec > >(tee /var/log/tf-user-data.log|logger -t user-data ) 2>&1 4 | 5 | logger() { 6 | DT=$(date '+%Y/%m/%d %H:%M:%S') 7 | echo "$DT $0: $1" 8 | } 9 | 10 | logger "Running" 11 | 12 | ##-------------------------------------------------------------------- 13 | ## Variables 14 | 15 | # Get Private IP address 16 | PRIVATE_IP=$(curl http://169.254.169.254/latest/meta-data/local-ipv4) 17 | 18 | VAULT_ZIP="${tpl_vault_zip_file}" 19 | 20 | AWS_REGION="${tpl_aws_region}" 21 | KMS_KEY="${tpl_kms_key}" 22 | 23 | ##-------------------------------------------------------------------- 24 | ## Functions 25 | 26 | user_ubuntu() { 27 | # UBUNTU user setup 28 | if ! getent group $${USER_GROUP} >/dev/null 29 | then 30 | sudo addgroup --system $${USER_GROUP} >/dev/null 31 | fi 32 | 33 | if ! getent passwd $${USER_NAME} >/dev/null 34 | then 35 | sudo adduser \ 36 | --system \ 37 | --disabled-login \ 38 | --ingroup $${USER_GROUP} \ 39 | --home $${USER_HOME} \ 40 | --no-create-home \ 41 | --gecos "$${USER_COMMENT}" \ 42 | --shell /bin/false \ 43 | $${USER_NAME} >/dev/null 44 | fi 45 | } 46 | 47 | ##-------------------------------------------------------------------- 48 | ## Install Base Prerequisites 49 | 50 | logger "Setting timezone to UTC" 51 | sudo timedatectl set-timezone UTC 52 | 53 | logger "Performing updates and installing prerequisites" 54 | sudo apt-get -qq -y update 55 | sudo apt-get install -qq -y wget unzip ntp jq 56 | sudo systemctl start ntp.service 57 | sudo systemctl enable ntp.service 58 | logger "Disable reverse dns lookup in SSH" 59 | sudo sh -c 'echo "\nUseDNS no" >> /etc/ssh/sshd_config' 60 | sudo service ssh restart 61 | 62 | ##-------------------------------------------------------------------- 63 | ## Configure Vault user 64 | 65 | USER_NAME="vault" 66 | USER_COMMENT="HashiCorp Vault user" 67 | USER_GROUP="vault" 68 | USER_HOME="/srv/vault" 69 | 70 | logger "Setting up user $${USER_NAME} for Debian/Ubuntu" 71 | user_ubuntu 72 | 73 | ##-------------------------------------------------------------------- 74 | ## Install Vault 75 | 76 | logger "Downloading Vault" 77 | curl -o /tmp/vault.zip $${VAULT_ZIP} 78 | 79 | logger "Installing Vault" 80 | sudo unzip -o /tmp/vault.zip -d /usr/local/bin/ 81 | sudo chmod 0755 /usr/local/bin/vault 82 | sudo chown vault:vault /usr/local/bin/vault 83 | sudo mkdir -pm 0755 /etc/vault.d 84 | sudo mkdir -pm 0755 /etc/ssl/vault 85 | 86 | logger "/usr/local/bin/vault --version: $(/usr/local/bin/vault --version)" 87 | 88 | logger "Configuring Vault" 89 | sudo tee /etc/vault.d/vault.hcl <> /etc/environment <:role/ 17 | ``` 18 | 19 | 1. Run `docker-compose`. It will exit with error code 0 if successful: 20 | 21 | ```sh 22 | docker-compose up --build --exit-code lambda 23 | ``` 24 | -------------------------------------------------------------------------------- /test/api/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | FROM docker.mirror.hashicorp.services/golang:1.15-alpine3.13 as builder 5 | 6 | COPY . /go/src/ 7 | WORKDIR /go/src/ 8 | 9 | RUN go build -o /bin/api main.go 10 | 11 | FROM docker.mirror.hashicorp.services/alpine:3.13 12 | 13 | COPY --from=builder /bin/api /bin/api 14 | 15 | ENTRYPOINT /bin/api -------------------------------------------------------------------------------- /test/api/README.md: -------------------------------------------------------------------------------- 1 | # `test/api` 2 | 3 | In AWS Lambda functions, both extensions and the function itself operate by 4 | requesting the next event from an HTTP API in an infinite loop. Each event 5 | returned is typically either an invocation, or for extensions, it could also 6 | be a request to shutdown. 7 | 8 | This folder contains a small API server to mock that AWS Lambda API for the 9 | purpose of local/CI integration tests. It's intentionally as small and as 10 | simplistic as possible to support the bare minimum to run extension 11 | binaries. 12 | 13 | In addition to the AWS Lambda APIs, there is also a /_sync endpoint used 14 | within the tests to synchronise on events such as the extension signalling 15 | readiness. 16 | -------------------------------------------------------------------------------- /test/api/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "net/http" 10 | "sync" 11 | ) 12 | 13 | func main() { 14 | // Extension API endpoints. 15 | http.HandleFunc("/2020-01-01/extension/register", extensionRegisterHandler) 16 | http.HandleFunc("/2020-01-01/extension/event/next", extensionEventHandler()) 17 | 18 | // Test framework synchronisation endpoints. 19 | // GETs will wait for a POST to the path. 20 | // POSTs return immediately. 21 | http.HandleFunc("/_sync/extension-initialised", syncHandler()) 22 | http.HandleFunc("/_sync/shutdown-extension", syncHandler()) 23 | 24 | log.Println("Mock API listening on port 80") 25 | err := http.ListenAndServe("0.0.0.0:80", nil) 26 | if err != nil { 27 | log.Printf("Error shutting down: %s\n", err) 28 | } 29 | } 30 | 31 | // extensionRegisterHandler just answers OK. 32 | func extensionRegisterHandler(w http.ResponseWriter, _ *http.Request) { 33 | log.Println("/extension/register API invoked") 34 | _, err := w.Write([]byte("{}")) 35 | if err != nil { 36 | http.Error(w, err.Error(), 500) 37 | return 38 | } 39 | } 40 | 41 | // extensionEventHandler returns an INVOKE event and then a SHUTDOWN event, with a wait in between. 42 | func extensionEventHandler() func(w http.ResponseWriter, _ *http.Request) { 43 | var mutex sync.Mutex 44 | var calls int 45 | return func(w http.ResponseWriter, _ *http.Request) { 46 | // Make this stateful handler explicitly single threaded. 47 | mutex.Lock() 48 | defer mutex.Unlock() 49 | 50 | log.Println("/extension/event/next API invoked") 51 | calls++ 52 | switch calls { 53 | case 1: 54 | // Tell the API that the extension has finished initialising. 55 | _, err := http.Post("http://127.0.0.1:80/_sync/extension-initialised", "", nil) 56 | if err != nil { 57 | http.Error(w, "Failed to create extension initialised event", 500) 58 | return 59 | } 60 | _, err = w.Write([]byte(`{"eventType":"INVOKE"}`)) 61 | if err != nil { 62 | http.Error(w, err.Error(), 500) 63 | return 64 | } 65 | log.Println("INVOKE event returned") 66 | case 2: 67 | // First, wait for shutdown to get requested. 68 | _, err := http.Get("http://127.0.0.1:80/_sync/shutdown-extension") 69 | if err != nil { 70 | http.Error(w, "Failed to wait for shutdown event", 500) 71 | return 72 | } 73 | _, err = w.Write([]byte(`{"eventType":"SHUTDOWN"}`)) 74 | if err != nil { 75 | http.Error(w, err.Error(), 500) 76 | return 77 | } 78 | log.Println("SHUTDOWN event returned") 79 | default: 80 | http.Error(w, "only expected /event/next endpoint to be called twice", 400) 81 | } 82 | } 83 | } 84 | 85 | func syncHandler() func(http.ResponseWriter, *http.Request) { 86 | ch := make(chan struct{}) 87 | return func(w http.ResponseWriter, r *http.Request) { 88 | log.Printf("%s to %s\n", r.Method, r.URL.Path) 89 | switch r.Method { 90 | case "POST": 91 | ch <- struct{}{} 92 | case "GET": 93 | <-ch 94 | default: 95 | http.Error(w, fmt.Sprintf("Unexpected sync method %s", r.Method), 400) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /test/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | version: "3.9" 5 | 6 | services: 7 | api: 8 | build: 9 | context: api/ 10 | lambda: 11 | build: 12 | context: ../ 13 | dockerfile: test/lambda/Dockerfile 14 | depends_on: 15 | - api 16 | - vault 17 | environment: 18 | # Configuration for vault-lambda-extension 19 | - VAULT_ADDR=http://vault:8200 20 | - VAULT_AUTH_PROVIDER=aws 21 | - VAULT_AUTH_ROLE=lambda-demo-function 22 | - VAULT_TOKEN_EXPIRY_GRACE_PERIOD=100ms 23 | # Configuration for the AWS extension client 24 | - AWS_LAMBDA_RUNTIME_API=api 25 | # Auth for vault-lambda-extension 26 | # These key-only env vars use the host machine's values 27 | - AWS_ROLE_ARN 28 | - AWS_ACCESS_KEY_ID 29 | - AWS_SECRET_ACCESS_KEY 30 | - AWS_SESSION_TOKEN 31 | - VAULT_LOG_LEVEL 32 | vault: 33 | image: docker.mirror.hashicorp.services/vault:1.6.2 34 | command: vault server -dev -log-level=err 35 | environment: 36 | - VAULT_DEV_ROOT_TOKEN_ID=root 37 | - VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200 -------------------------------------------------------------------------------- /test/lambda/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | FROM docker.mirror.hashicorp.services/vault:1.6.2 as vault 5 | 6 | FROM docker.mirror.hashicorp.services/golang:1.15-alpine3.13 as builder 7 | 8 | COPY . /go/src/ 9 | WORKDIR /go/src/ 10 | 11 | RUN go build -o /bin/vault-lambda-extension main.go 12 | 13 | FROM docker.mirror.hashicorp.services/alpine:3.13 14 | 15 | RUN apk update && apk add curl 16 | 17 | COPY --from=vault /bin/vault /bin/vault 18 | COPY --from=builder /bin/vault-lambda-extension /opt/extensions/vault-lambda-extension 19 | COPY test/lambda/runtime.sh /bin/runtime.sh 20 | 21 | ENTRYPOINT ["/bin/runtime.sh"] -------------------------------------------------------------------------------- /test/lambda/runtime.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | # This script is for local integration testing. 6 | # It simulates a basic version of how Lambda runs a function that includes extensions. 7 | 8 | set -euo pipefail 9 | 10 | function configure_vault() { 11 | # Wait until vault is up (retry up to 10 x 1s). 12 | curl --silent --retry 10 --retry-connrefused --retry-delay 1 $VAULT_ADDR > /dev/null 2>&1 13 | 14 | export VAULT_TOKEN=root 15 | vault policy write test_policy - <&1 | tee /tmp/vault-lambda-extension.log & 40 | EXTENSION_PID=$! 41 | 42 | # Wait for the extension to call /event/next API endpoint, signalling the end of the 43 | # extension initialisation phase. 44 | echo "Waiting for the extension to signal readiness" 45 | curl --silent --max-time 10 api:80/_sync/extension-initialised 46 | 47 | # Here is where the function code itself would now be invoked. 48 | # Instead of running a function, we run some tests on the extension. 49 | 50 | # Check the extension proxy is working (auth-less call to localhost). 51 | # Also ensure it continues to work beyond the original login token's TTL 52 | # and beyond the original token's max TTL. 53 | echo "Reading secret via proxy server" 54 | curl --silent -H "X-Vault-Request: true" http://127.0.0.1:8200/v1/secret/data/foo 55 | sleep 5 56 | 57 | # This loop spans a period where we should see a renewal and the end of the max token TTL. 58 | for i in `seq 15`; do 59 | curl --silent -H "X-Vault-Request: true" http://127.0.0.1:8200/v1/secret/data/foo > /dev/null 60 | sleep 1 61 | done 62 | 63 | # Tell the API that we're ready for it to send the SHUTDOWN event to the extension. 64 | echo "Signalling shutdown to extension" 65 | curl --silent --request POST api:80/_sync/shutdown-extension 66 | 67 | # Wait for the extension to finish shutting down. 68 | wait $EXTENSION_PID 69 | --------------------------------------------------------------------------------