├── .github ├── ISSUE_TEMPLATE.md └── workflows │ ├── build.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 ├── helper ├── agent.go └── agent_test.go ├── main.go ├── main_test.go ├── scripts ├── build.sh ├── cross │ └── Dockerfile ├── dist.sh └── update_deps.sh ├── test-fixtures └── config.hcl └── version.go /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [workflow_dispatch, push] 4 | 5 | env: 6 | PKG_NAME: "vault-ssh-helper" 7 | 8 | jobs: 9 | get-product-version: 10 | runs-on: ubuntu-latest 11 | outputs: 12 | product-version: ${{ steps.get-product-version.outputs.product-version }} 13 | steps: 14 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 15 | - name: get product version 16 | id: get-product-version 17 | run: | 18 | make version 19 | echo "product-version=$(make version)" >> "${GITHUB_OUTPUT}" 20 | 21 | generate-metadata-file: 22 | needs: get-product-version 23 | runs-on: ubuntu-latest 24 | outputs: 25 | filepath: ${{ steps.generate-metadata-file.outputs.filepath }} 26 | steps: 27 | - name: "Checkout directory" 28 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 29 | - name: Generate metadata file 30 | id: generate-metadata-file 31 | uses: hashicorp/actions-generate-metadata@v1 32 | with: 33 | version: ${{ needs.get-product-version.outputs.product-version }} 34 | product: ${{ env.PKG_NAME }} 35 | repositoryOwner: "hashicorp" 36 | - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 37 | with: 38 | name: metadata.json 39 | path: ${{ steps.generate-metadata-file.outputs.filepath }} 40 | 41 | build: 42 | needs: 43 | - get-product-version 44 | runs-on: ubuntu-latest 45 | strategy: 46 | matrix: 47 | goos: [linux] 48 | goarch: ["arm", "arm64", "386", "amd64"] 49 | 50 | fail-fast: true 51 | 52 | name: Go ${{ matrix.goos }} ${{ matrix.goarch }} build 53 | 54 | steps: 55 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 56 | 57 | - name: Setup go 58 | uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 59 | with: 60 | go-version-file: '.go-version' 61 | 62 | - name: Build 63 | env: 64 | GOOS: ${{ matrix.goos }} 65 | GOARCH: ${{ matrix.goarch }} 66 | run: | 67 | mkdir dist out 68 | make build 69 | zip -r -j out/${{ env.PKG_NAME }}_${{ needs.get-product-version.outputs.product-version }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip dist/ 70 | 71 | - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 72 | with: 73 | name: ${{ env.PKG_NAME }}_${{ needs.get-product-version.outputs.product-version }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip 74 | path: out/${{ env.PKG_NAME }}_${{ needs.get-product-version.outputs.product-version }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip 75 | -------------------------------------------------------------------------------- /.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 | 10 | jobs: 11 | sync: 12 | uses: hashicorp/vault-workflows-common/.github/workflows/jira.yaml@main 13 | secrets: 14 | JIRA_SYNC_BASE_URL: ${{ secrets.JIRA_SYNC_BASE_URL }} 15 | JIRA_SYNC_USER_EMAIL: ${{ secrets.JIRA_SYNC_USER_EMAIL }} 16 | JIRA_SYNC_API_TOKEN: ${{ secrets.JIRA_SYNC_API_TOKEN }} 17 | with: 18 | teams-array: '["cryptosec"]' 19 | -------------------------------------------------------------------------------- /.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 | test: 8 | runs-on: ubuntu-latest 9 | env: 10 | GO111MODULE: on 11 | steps: 12 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 13 | 14 | # cache/restore go mod 15 | - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 16 | with: 17 | path: | 18 | ~/.cache/go-build 19 | ~/go/pkg/mod 20 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 21 | restore-keys: | 22 | ${{ runner.os }}-go- 23 | 24 | - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 25 | with: 26 | go-version-file: '.go-version' 27 | 28 | - name: Install gotestsum 29 | run: go install gotest.tools/gotestsum@v1.7.0 30 | 31 | - name: Test 32 | run: | 33 | mkdir -p test-results/go 34 | make test-ci 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | bin/ 27 | pkg/ 28 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.21.4 2 | -------------------------------------------------------------------------------- /.release/ci.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | schema = "1" 5 | 6 | project "vault-ssh-helper" { 7 | team = "vault" 8 | slack { 9 | notification_channel = "C03RXFX5M4L" // #feed-vault-releases 10 | } 11 | github { 12 | organization = "hashicorp" 13 | repository = "vault-ssh-helper" 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-ssh-helper" 28 | workflow = "build" 29 | } 30 | } 31 | 32 | event "upload-dev" { 33 | depends = ["build"] 34 | action "upload-dev" { 35 | organization = "hashicorp" 36 | repository = "crt-workflows-common" 37 | workflow = "upload-dev" 38 | depends = ["build"] 39 | } 40 | 41 | notification { 42 | on = "fail" 43 | } 44 | } 45 | 46 | event "security-scan-binaries" { 47 | depends = ["upload-dev"] 48 | action "security-scan-binaries" { 49 | organization = "hashicorp" 50 | repository = "crt-workflows-common" 51 | workflow = "security-scan-binaries" 52 | config = "security-scan.hcl" 53 | } 54 | 55 | notification { 56 | on = "fail" 57 | } 58 | } 59 | 60 | event "sign" { 61 | depends = ["security-scan-binaries"] 62 | action "sign" { 63 | organization = "hashicorp" 64 | repository = "crt-workflows-common" 65 | workflow = "sign" 66 | } 67 | 68 | notification { 69 | on = "fail" 70 | } 71 | } 72 | 73 | event "verify" { 74 | depends = ["sign"] 75 | action "verify" { 76 | organization = "hashicorp" 77 | repository = "crt-workflows-common" 78 | workflow = "verify" 79 | } 80 | 81 | notification { 82 | on = "fail" 83 | } 84 | } 85 | 86 | ## These are promotion and post-publish events 87 | ## they should be added to the end of the file after the verify event stanza. 88 | 89 | event "trigger-staging" { 90 | // This event is dispatched by the bob trigger-promotion command 91 | // and is required - do not delete. 92 | } 93 | 94 | event "promote-staging" { 95 | depends = ["trigger-staging"] 96 | action "promote-staging" { 97 | organization = "hashicorp" 98 | repository = "crt-workflows-common" 99 | workflow = "promote-staging" 100 | config = "release-metadata.hcl" 101 | } 102 | 103 | notification { 104 | on = "always" 105 | } 106 | } 107 | 108 | event "trigger-production" { 109 | // This event is dispatched by the bob trigger-promotion command 110 | // and is required - do not delete. 111 | } 112 | 113 | event "promote-production" { 114 | depends = ["trigger-production"] 115 | action "promote-production" { 116 | organization = "hashicorp" 117 | repository = "crt-workflows-common" 118 | workflow = "promote-production" 119 | } 120 | 121 | notification { 122 | on = "always" 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /.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-ssh-helper/blob/main/LICENSE" 5 | url_project_website = "https://www.vaultproject.io/docs/secrets/ssh/one-time-ssh-passwords" 6 | url_source_repository = "https://github.com/hashicorp/vault-ssh-helper" -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | IMPROVEMENTS: 4 | 5 | * Added `-log-level` command-line option [GH-77](https://github.com/hashicorp/vault-ssh-helper/pull/77) 6 | 7 | CHANGES: 8 | 9 | * Building with Go 1.21.4 10 | * Updated golang dependencies [GH-71](https://github.com/hashicorp/vault-ssh-helper/pull/71) 11 | * golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 => v0.16.0 12 | * golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c => v0.15.0 13 | * golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 => v0.19.0 14 | * golang.org/x/text v0.3.3 => v0.14.0 15 | * github.com/hashicorp/go-hclog v1.4.0 => v1.5.0 16 | * github.com/hashicorp/go-uuid v1.0.2 => v1.0.3 17 | * github.com/hashicorp/vault/api v1.4.1 => v1.10.0 18 | * github.com/go-jose/go-jose/v3 v3.0.0 => v3.0.1 19 | 20 | ## 0.2.1 (December 15, 2020) 21 | 22 | BUG FIXES: 23 | 24 | * Update ssh-helper's `Version` to properly reflect its release version 25 | 26 | ## 0.2.0 (August 19, 2020) 27 | 28 | SECURITY: 29 | 30 | - HashiCorp vault-ssh-helper up to and including version 0.1.6 incorrectly accepted Vault-issued 31 | SSH OTPs for the subnet in which a host's network interface was located, rather than the specific IP address 32 | assigned to that interface. Assigned CVE-2020-24359, fixed in 0.2.0. 33 | 34 | ## 0.1.6 (June 26, 2020) 35 | 36 | FEATURES: 37 | 38 | * Add support for namespaces [GH-44](https://github.com/hashicorp/vault-ssh-helper/pull/44) 39 | 40 | 41 | ## 0.1.4 (November 8 2017) 42 | 43 | SECURITY: 44 | 45 | * Make a safe exit when displaying usage text [GH-32] 46 | 47 | ## 0.1.3 (February 8 2017) 48 | 49 | SECURITY: 50 | 51 | * Verify that OTPs conform to UUID format [7a831a5] 52 | 53 | ## 0.1.2 (August 24 2016) 54 | 55 | IMPROVEMENTS: 56 | 57 | * Added `allowed_roles` option to configuration, which enforces specified 58 | role names to be present in the verification response received by the agent. 59 | 60 | UPGRADE NOTES: 61 | 62 | * The option `allowed_roles` is a breaking change. When vault-ssh-helper 63 | is upgraded, it is required that the existing configuration files have 64 | an entry for `allowed_roles="*"` to be backwards compatible. 65 | 66 | ## 0.1.1 (February 25 2016) 67 | 68 | SECURITY: 69 | 70 | * Introduced `dev` mode. If `dev` mode is not activated, `vault-ssh-helper` 71 | can only communicate with Vault that has TLS enabled [f7a8707] 72 | 73 | IMPROVEMENTS: 74 | 75 | * Updated the documentation [GH-12] 76 | 77 | BUG FIXES: 78 | 79 | * Empty check for `allowed_cidr_list` [9acaa58] 80 | 81 | ## 0.1.0 (December 2 2015) 82 | 83 | * Initial release 84 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hashicorp/vault-crypto 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 HashiCorp, Inc. 2 | 3 | Mozilla Public License, version 2.0 4 | 5 | 1. Definitions 6 | 7 | 1.1. "Contributor" 8 | 9 | means each individual or legal entity that creates, contributes to the 10 | creation of, or owns Covered Software. 11 | 12 | 1.2. "Contributor Version" 13 | 14 | means the combination of the Contributions of others (if any) used by a 15 | Contributor and that particular Contributor's Contribution. 16 | 17 | 1.3. "Contribution" 18 | 19 | means Covered Software of a particular Contributor. 20 | 21 | 1.4. "Covered Software" 22 | 23 | means Source Code Form to which the initial Contributor has attached the 24 | notice in Exhibit A, the Executable Form of such Source Code Form, and 25 | Modifications of such Source Code Form, in each case including portions 26 | thereof. 27 | 28 | 1.5. "Incompatible With Secondary Licenses" 29 | means 30 | 31 | a. that the initial Contributor has attached the notice described in 32 | Exhibit B to the Covered Software; or 33 | 34 | b. that the Covered Software was made available under the terms of 35 | version 1.1 or earlier of the License, but not also under the terms of 36 | a Secondary License. 37 | 38 | 1.6. "Executable Form" 39 | 40 | means any form of the work other than Source Code Form. 41 | 42 | 1.7. "Larger Work" 43 | 44 | means a work that combines Covered Software with other material, in a 45 | separate file or files, that is not Covered Software. 46 | 47 | 1.8. "License" 48 | 49 | means this document. 50 | 51 | 1.9. "Licensable" 52 | 53 | means having the right to grant, to the maximum extent possible, whether 54 | at the time of the initial grant or subsequently, any and all of the 55 | rights conveyed by this License. 56 | 57 | 1.10. "Modifications" 58 | 59 | means any of the following: 60 | 61 | a. any file in Source Code Form that results from an addition to, 62 | deletion from, or modification of the contents of Covered Software; or 63 | 64 | b. any new file in Source Code Form that contains any Covered Software. 65 | 66 | 1.11. "Patent Claims" of a Contributor 67 | 68 | means any patent claim(s), including without limitation, method, 69 | process, and apparatus claims, in any patent Licensable by such 70 | Contributor that would be infringed, but for the grant of the License, 71 | by the making, using, selling, offering for sale, having made, import, 72 | or transfer of either its Contributions or its Contributor Version. 73 | 74 | 1.12. "Secondary License" 75 | 76 | means either the GNU General Public License, Version 2.0, the GNU Lesser 77 | General Public License, Version 2.1, the GNU Affero General Public 78 | License, Version 3.0, or any later versions of those licenses. 79 | 80 | 1.13. "Source Code Form" 81 | 82 | means the form of the work preferred for making modifications. 83 | 84 | 1.14. "You" (or "Your") 85 | 86 | means an individual or a legal entity exercising rights under this 87 | License. For legal entities, "You" includes any entity that controls, is 88 | controlled by, or is under common control with You. For purposes of this 89 | definition, "control" means (a) the power, direct or indirect, to cause 90 | the direction or management of such entity, whether by contract or 91 | otherwise, or (b) ownership of more than fifty percent (50%) of the 92 | outstanding shares or beneficial ownership of such entity. 93 | 94 | 95 | 2. License Grants and Conditions 96 | 97 | 2.1. Grants 98 | 99 | Each Contributor hereby grants You a world-wide, royalty-free, 100 | non-exclusive license: 101 | 102 | a. under intellectual property rights (other than patent or trademark) 103 | Licensable by such Contributor to use, reproduce, make available, 104 | modify, display, perform, distribute, and otherwise exploit its 105 | Contributions, either on an unmodified basis, with Modifications, or 106 | as part of a Larger Work; and 107 | 108 | b. under Patent Claims of such Contributor to make, use, sell, offer for 109 | sale, have made, import, and otherwise transfer either its 110 | Contributions or its Contributor Version. 111 | 112 | 2.2. Effective Date 113 | 114 | The licenses granted in Section 2.1 with respect to any Contribution 115 | become effective for each Contribution on the date the Contributor first 116 | distributes such Contribution. 117 | 118 | 2.3. Limitations on Grant Scope 119 | 120 | The licenses granted in this Section 2 are the only rights granted under 121 | this License. No additional rights or licenses will be implied from the 122 | distribution or licensing of Covered Software under this License. 123 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 124 | Contributor: 125 | 126 | a. for any code that a Contributor has removed from Covered Software; or 127 | 128 | b. for infringements caused by: (i) Your and any other third party's 129 | modifications of Covered Software, or (ii) the combination of its 130 | Contributions with other software (except as part of its Contributor 131 | Version); or 132 | 133 | c. under Patent Claims infringed by Covered Software in the absence of 134 | its Contributions. 135 | 136 | This License does not grant any rights in the trademarks, service marks, 137 | or logos of any Contributor (except as may be necessary to comply with 138 | the notice requirements in Section 3.4). 139 | 140 | 2.4. Subsequent Licenses 141 | 142 | No Contributor makes additional grants as a result of Your choice to 143 | distribute the Covered Software under a subsequent version of this 144 | License (see Section 10.2) or under the terms of a Secondary License (if 145 | permitted under the terms of Section 3.3). 146 | 147 | 2.5. Representation 148 | 149 | Each Contributor represents that the Contributor believes its 150 | Contributions are its original creation(s) or it has sufficient rights to 151 | grant the rights to its Contributions conveyed by this License. 152 | 153 | 2.6. Fair Use 154 | 155 | This License is not intended to limit any rights You have under 156 | applicable copyright doctrines of fair use, fair dealing, or other 157 | equivalents. 158 | 159 | 2.7. Conditions 160 | 161 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 162 | Section 2.1. 163 | 164 | 165 | 3. Responsibilities 166 | 167 | 3.1. Distribution of Source Form 168 | 169 | All distribution of Covered Software in Source Code Form, including any 170 | Modifications that You create or to which You contribute, must be under 171 | the terms of this License. You must inform recipients that the Source 172 | Code Form of the Covered Software is governed by the terms of this 173 | License, and how they can obtain a copy of this License. You may not 174 | attempt to alter or restrict the recipients' rights in the Source Code 175 | Form. 176 | 177 | 3.2. Distribution of Executable Form 178 | 179 | If You distribute Covered Software in Executable Form then: 180 | 181 | a. such Covered Software must also be made available in Source Code Form, 182 | as described in Section 3.1, and You must inform recipients of the 183 | Executable Form how they can obtain a copy of such Source Code Form by 184 | reasonable means in a timely manner, at a charge no more than the cost 185 | of distribution to the recipient; and 186 | 187 | b. You may distribute such Executable Form under the terms of this 188 | License, or sublicense it under different terms, provided that the 189 | license for the Executable Form does not attempt to limit or alter the 190 | recipients' rights in the Source Code Form under this License. 191 | 192 | 3.3. Distribution of a Larger Work 193 | 194 | You may create and distribute a Larger Work under terms of Your choice, 195 | provided that You also comply with the requirements of this License for 196 | the Covered Software. If the Larger Work is a combination of Covered 197 | Software with a work governed by one or more Secondary Licenses, and the 198 | Covered Software is not Incompatible With Secondary Licenses, this 199 | License permits You to additionally distribute such Covered Software 200 | under the terms of such Secondary License(s), so that the recipient of 201 | the Larger Work may, at their option, further distribute the Covered 202 | Software under the terms of either this License or such Secondary 203 | License(s). 204 | 205 | 3.4. Notices 206 | 207 | You may not remove or alter the substance of any license notices 208 | (including copyright notices, patent notices, disclaimers of warranty, or 209 | limitations of liability) contained within the Source Code Form of the 210 | Covered Software, except that You may alter any license notices to the 211 | extent required to remedy known factual inaccuracies. 212 | 213 | 3.5. Application of Additional Terms 214 | 215 | You may choose to offer, and to charge a fee for, warranty, support, 216 | indemnity or liability obligations to one or more recipients of Covered 217 | Software. However, You may do so only on Your own behalf, and not on 218 | behalf of any Contributor. You must make it absolutely clear that any 219 | such warranty, support, indemnity, or liability obligation is offered by 220 | You alone, and You hereby agree to indemnify every Contributor for any 221 | liability incurred by such Contributor as a result of warranty, support, 222 | indemnity or liability terms You offer. You may include additional 223 | disclaimers of warranty and limitations of liability specific to any 224 | jurisdiction. 225 | 226 | 4. Inability to Comply Due to Statute or Regulation 227 | 228 | If it is impossible for You to comply with any of the terms of this License 229 | with respect to some or all of the Covered Software due to statute, 230 | judicial order, or regulation then You must: (a) comply with the terms of 231 | this License to the maximum extent possible; and (b) describe the 232 | limitations and the code they affect. Such description must be placed in a 233 | text file included with all distributions of the Covered Software under 234 | this License. Except to the extent prohibited by statute or regulation, 235 | such description must be sufficiently detailed for a recipient of ordinary 236 | skill to be able to understand it. 237 | 238 | 5. Termination 239 | 240 | 5.1. The rights granted under this License will terminate automatically if You 241 | fail to comply with any of its terms. However, if You become compliant, 242 | then the rights granted under this License from a particular Contributor 243 | are reinstated (a) provisionally, unless and until such Contributor 244 | explicitly and finally terminates Your grants, and (b) on an ongoing 245 | basis, if such Contributor fails to notify You of the non-compliance by 246 | some reasonable means prior to 60 days after You have come back into 247 | compliance. Moreover, Your grants from a particular Contributor are 248 | reinstated on an ongoing basis if such Contributor notifies You of the 249 | non-compliance by some reasonable means, this is the first time You have 250 | received notice of non-compliance with this License from such 251 | Contributor, and You become compliant prior to 30 days after Your receipt 252 | of the notice. 253 | 254 | 5.2. If You initiate litigation against any entity by asserting a patent 255 | infringement claim (excluding declaratory judgment actions, 256 | counter-claims, and cross-claims) alleging that a Contributor Version 257 | directly or indirectly infringes any patent, then the rights granted to 258 | You by any and all Contributors for the Covered Software under Section 259 | 2.1 of this License shall terminate. 260 | 261 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 262 | license agreements (excluding distributors and resellers) which have been 263 | validly granted by You or Your distributors under this License prior to 264 | termination shall survive termination. 265 | 266 | 6. Disclaimer of Warranty 267 | 268 | Covered Software is provided under this License on an "as is" basis, 269 | without warranty of any kind, either expressed, implied, or statutory, 270 | including, without limitation, warranties that the Covered Software is free 271 | of defects, merchantable, fit for a particular purpose or non-infringing. 272 | The entire risk as to the quality and performance of the Covered Software 273 | is with You. Should any Covered Software prove defective in any respect, 274 | You (not any Contributor) assume the cost of any necessary servicing, 275 | repair, or correction. This disclaimer of warranty constitutes an essential 276 | part of this License. No use of any Covered Software is authorized under 277 | this License except under this disclaimer. 278 | 279 | 7. Limitation of Liability 280 | 281 | Under no circumstances and under no legal theory, whether tort (including 282 | negligence), contract, or otherwise, shall any Contributor, or anyone who 283 | distributes Covered Software as permitted above, be liable to You for any 284 | direct, indirect, special, incidental, or consequential damages of any 285 | character including, without limitation, damages for lost profits, loss of 286 | goodwill, work stoppage, computer failure or malfunction, or any and all 287 | other commercial damages or losses, even if such party shall have been 288 | informed of the possibility of such damages. This limitation of liability 289 | shall not apply to liability for death or personal injury resulting from 290 | such party's negligence to the extent applicable law prohibits such 291 | limitation. Some jurisdictions do not allow the exclusion or limitation of 292 | incidental or consequential damages, so this exclusion and limitation may 293 | not apply to You. 294 | 295 | 8. Litigation 296 | 297 | Any litigation relating to this License may be brought only in the courts 298 | of a jurisdiction where the defendant maintains its principal place of 299 | business and such litigation shall be governed by laws of that 300 | jurisdiction, without reference to its conflict-of-law provisions. Nothing 301 | in this Section shall prevent a party's ability to bring cross-claims or 302 | counter-claims. 303 | 304 | 9. Miscellaneous 305 | 306 | This License represents the complete agreement concerning the subject 307 | matter hereof. If any provision of this License is held to be 308 | unenforceable, such provision shall be reformed only to the extent 309 | necessary to make it enforceable. Any law or regulation which provides that 310 | the language of a contract shall be construed against the drafter shall not 311 | be used to construe this License against a Contributor. 312 | 313 | 314 | 10. Versions of the License 315 | 316 | 10.1. New Versions 317 | 318 | Mozilla Foundation is the license steward. Except as provided in Section 319 | 10.3, no one other than the license steward has the right to modify or 320 | publish new versions of this License. Each version will be given a 321 | distinguishing version number. 322 | 323 | 10.2. Effect of New Versions 324 | 325 | You may distribute the Covered Software under the terms of the version 326 | of the License under which You originally received the Covered Software, 327 | or under the terms of any subsequent version published by the license 328 | steward. 329 | 330 | 10.3. Modified Versions 331 | 332 | If you create software not governed by this License, and you want to 333 | create a new license for such software, you may create and use a 334 | modified version of this License if you rename the license and remove 335 | any references to the name of the license steward (except to note that 336 | such modified license differs from this License). 337 | 338 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 339 | Licenses If You choose to distribute Source Code Form that is 340 | Incompatible With Secondary Licenses under the terms of this version of 341 | the License, the notice described in Exhibit B of this License must be 342 | attached. 343 | 344 | Exhibit A - Source Code Form License Notice 345 | 346 | This Source Code Form is subject to the 347 | terms of the Mozilla Public License, v. 348 | 2.0. If a copy of the MPL was not 349 | distributed with this file, You can 350 | obtain one at 351 | http://mozilla.org/MPL/2.0/. 352 | 353 | If it is not possible or desirable to put the notice in a particular file, 354 | then You may include the notice in a location (such as a LICENSE file in a 355 | relevant directory) where a recipient would be likely to look for such a 356 | notice. 357 | 358 | You may add additional accurate notices of copyright ownership. 359 | 360 | Exhibit B - "Incompatible With Secondary Licenses" Notice 361 | 362 | This Source Code Form is "Incompatible 363 | With Secondary Licenses", as defined by 364 | the Mozilla Public License, v. 2.0. 365 | 366 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TEST?=$$(go list ./...) 2 | NAME?=$(shell basename "$(CURDIR)") 3 | VERSION=$(shell awk -F\" '/^const Version/ { print $$2; exit }' version.go) 4 | GIT_COMMIT=$(shell git rev-parse HEAD) 5 | GIT_DIRTY=$(shell test -n "`git status --porcelain`" && echo "+CHANGES" || true) 6 | LD_FLAGS="-X github.com/hashicorp/vault-ssh-helper/main.GitCommit=$(GIT_COMMIT)$(GIT_DIRTY)" 7 | 8 | default: dev 9 | 10 | # bin generates the releaseable binaries for Vault 11 | bin: 12 | @sh -c "'$(CURDIR)/scripts/build.sh'" 13 | 14 | # dev creates binaries for testing Vault locally. These are put 15 | # into ./bin/ as well as $GOPATH/bin 16 | dev: 17 | @DEV=1 sh -c "'$(CURDIR)/scripts/build.sh'" 18 | 19 | # dist creates the binaries for distibution 20 | dist: bin 21 | @sh -c "'$(CURDIR)/scripts/dist.sh' $(VERSION)" 22 | 23 | # test runs the unit tests and vets the code 24 | test: 25 | TF_ACC= go test $(TEST) $(TESTARGS) -timeout=30s -parallel=4 26 | 27 | test-ci: 28 | gotestsum --format=short-verbose --junitfile test-results/go/results.xml -- $(TEST) $(TESTARGS) -timeout=30s -parallel=4 29 | 30 | # testacc runs acceptance tests 31 | testacc: 32 | @if [ "$(TEST)" = "./..." ]; then \ 33 | echo "ERROR: Set TEST to a specific package"; \ 34 | exit 1; \ 35 | fi 36 | TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 45m 37 | 38 | # testrace runs the race checker 39 | testrace: 40 | TF_ACC= go test -race $(TEST) $(TESTARGS) 41 | 42 | # vet runs the Go source code static analysis tool `vet` to find 43 | # any common errors. 44 | vet: 45 | @go vet 2>/dev/null ; if [ $$? -eq 3 ]; then \ 46 | go get golang.org/x/tools/cmd/vet; \ 47 | fi 48 | @go list -f '{{.Dir}}' ./... \ 49 | | grep -v '.*github.com/hashicorp/vault-ssh-helper$$' \ 50 | | xargs go vet ; if [ $$? -eq 1 ]; then \ 51 | echo ""; \ 52 | echo "Vet found suspicious constructs. Please check the reported constructs"; \ 53 | echo "and fix them if necessary before submitting the code for reviewal."; \ 54 | fi 55 | 56 | # updatedeps installs all the dependencies needed to run and build - this is 57 | # specifically designed to only pull deps, but not self. 58 | updatedeps: 59 | GO111MODULE=off go get -u github.com/mitchellh/gox 60 | echo $$(go list ./...) \ 61 | | xargs go list -f '{{ join .Deps "\n" }}{{ printf "\n" }}{{ join .TestImports "\n" }}' \ 62 | | grep -v github.com/hashicorp/$(NAME) \ 63 | | xargs go get -f -u -v 64 | 65 | install: dev 66 | @sudo cp bin/vault-ssh-helper /usr/local/bin 67 | 68 | .PHONY: default bin dev dist test vet updatedeps testacc install 69 | 70 | # This is used for release builds by .github/workflows/build.yml 71 | .PHONY: version 72 | version: 73 | @echo $(VERSION) 74 | 75 | .PHONY: build 76 | # This is used for release builds by .github/workflows/build.yml 77 | build: 78 | @echo "--> Building $(NAME) $(VERSION)" 79 | @go build -v -ldflags $(LD_FLAGS) -o dist/ 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | vault-ssh-helper[![Build Status](https://travis-ci.org/hashicorp/vault-ssh-helper.svg)](https://travis-ci.org/hashicorp/vault-ssh-helper) 2 | =============== 3 | 4 | **Please note**: We take Vault's security and our users' trust very seriously. If you believe you have found a security issue in Vault, _please responsibly disclose_ by contacting us at [security@hashicorp.com](mailto:security@hashicorp.com). 5 | 6 | ---- 7 | 8 | `vault-ssh-helper` is a counterpart to [HashiCorp 9 | Vault's](https://github.com/hashicorp/vault) SSH backend. It allows a machine 10 | to consume One-Time-Passwords (OTP) created by Vault servers by allowing them 11 | to be used as client authentication credentials at SSH connection time. 12 | 13 | All of the remote hosts that belong to the SSH backend's OTP-type roles will 14 | need this helper installed. In addition, each host must have its SSH 15 | configuration changed to enable keyboard-interactive authentication and 16 | redirect its client authentication responsibility to `vault-ssh-helper`. 17 | 18 | Vault-authenticated users contact the Vault server and retrieve an OTP issued 19 | for a specific username and IP address. While establishing an SSH connection to 20 | the host, the `vault-ssh-helper` binary reads the OTP from the password prompt 21 | and sends it to the Vault server for verification. Client authentication is 22 | successful (and the SSH connection allowed) only if the Vault server verifies 23 | the OTP. True to its name, once the OTP has been used a single time for 24 | authentication, it is removed from Vault and cannot be used again. 25 | 26 | `vault-ssh-helper` is not a PAM module, but it does the job of one. 27 | `vault-ssh-helper`'s binary is run as an external command using `pam_exec.so` 28 | with access to the entered password (in this case, the issued OTP). Successful 29 | execution and exit of this command is a PAM 'requisite' for authentication to 30 | be successful. If the OTP is not validated, the binary exits with a non-zero 31 | status and authentication fails. 32 | 33 | PAM modules are generally shared object files; rather than writing and 34 | maintaining a PAM module in C, `vault-ssh-helper` is written in Go and invoked 35 | as an external binary. This allows `vault-ssh-helper` to be contained within 36 | one code base with known, testable behavior. It also allows other 37 | authentication systems that are not PAM-based to invoke `vault-ssh-helper` and 38 | take advantage of its capabilities. 39 | 40 | ## Usage 41 | ----- 42 | `vault-ssh-helper [options]` 43 | 44 | ### Options 45 | | Option | Description | 46 | |---------------|-----------------------------------------------------------------------------------------------------------------------------| 47 | | `verify-only` | Verifies that `vault-ssh-helper` is installed correctly and is able to communicate with Vault. | 48 | | `config` | The path to the configuration file. Configuration options are detailed below. | 49 | | `dev` | `vault-ssh-helper` communicates with Vault with TLS disabled. This is NOT recommended for production use. Use with caution. | 50 | | `log-level` | Level of logs to output. Defaults to `info`. Supported values are `off`, `trace`, `debug`, `info`, `warn`, and `error`. | 51 | 52 | ## Download vault-ssh-helper 53 | 54 | Download the latest version of `vault-ssh-helper` at [releases.hashicorp.com](https://releases.hashicorp.com/vault-ssh-helper). 55 | 56 | ## Build and Install 57 | ----- 58 | 59 | You'll first need Go installed on your machine (version 1.8+ is required). 60 | 61 | Install `Go` on your machine and set `GOPATH` accordingly. Clone this 62 | repository into $GOPATH/src/github.com/hashicorp/vault-ssh-helper. Install all 63 | of the dependent binaries like `godep`, `gox`, `vet`, etc. by bootstrapping the 64 | environment: 65 | 66 | ```shell 67 | $ make updatedeps 68 | ``` 69 | 70 | Build and install `vault-ssh-helper`: 71 | 72 | ```shell 73 | $ make 74 | $ make install 75 | ``` 76 | 77 | Follow the instructions below to modify your SSH server configuration, PAM 78 | configuration and `vault-ssh-helper` configuration. Check if `vault-ssh-helper` 79 | is installed and configured correctly and also is able to communicate with 80 | Vault server properly. Before verifying `vault-ssh-helper`, make sure that the 81 | Vault server is up and running and it has mounted the SSH backend. Also, make 82 | sure that the mount path of the SSH backend is properly updated in 83 | `vault-ssh-helper`'s config file: 84 | 85 | ```shell 86 | $ vault-ssh-helper -verify-only -config= 87 | Using SSH Mount point: ssh 88 | vault-ssh-helper verification successful! 89 | ``` 90 | 91 | If you intend to contribute to this project, compile a development version of 92 | `vault-ssh-helper` using `make dev`. This will put the binary in the `bin` and 93 | `$GOPATH/bin` folders. 94 | 95 | ```shell 96 | $ make dev 97 | ``` 98 | 99 | If you're developing a specific package, you can run tests for just that 100 | package by specifying the `TEST` variable. For example below, only `helper` 101 | package tests will be run. 102 | 103 | ```sh 104 | $ make test TEST=./helper 105 | ... 106 | ``` 107 | 108 | If you intend to cross compile the binary, run `make bin`. 109 | 110 | `vault-ssh-helper` Configuration 111 | ------------------- 112 | **[Note]: This configuration is applicable for Ubuntu 14.04. SSH/PAM 113 | configurations differ with each platform and distribution.** 114 | 115 | `vault-ssh-helper`'s configuration is written in [HashiCorp Configuration 116 | Language (HCL)](https://github.com/hashicorp/hcl). By proxy, this means that 117 | `vault-ssh-helper`'s configuration is JSON-compatible. For more information, 118 | please see the [HCL Specification](https://github.com/hashicorp/hcl). 119 | 120 | ### Properties 121 | |Property |Description| 122 | |-------------------|-----------| 123 | |`vault_addr` |[Required] Address of the Vault server. 124 | |`ssh_mount_point` |[Required] Mount point of SSH backend in Vault server. 125 | |`namespace` |Namespace of the SSH mount. (Vault Enterprise only) 126 | |`ca_cert` |Path of a PEM-encoded CA certificate file used to verify the Vault server's TLS certificate. `-dev` mode ignores this value. 127 | |`ca_path` |Path to directory of PEM-encoded CA certificate files used to verify the Vault server's TLS certiciate. `-dev` mode ignores this value. 128 | |`tls_skip_verify` |Skip TLS certificate verification. Use with caution. 129 | |`allowed_roles` |List of comma-separated Vault SSH roles. The OTP verification response from the server will contain the name of the role against which the OTP was issued. Specify which roles are allowed to login using this configuration. Set this to `*` to allow any role to perform a login. 130 | |`allowed_cidr_list`|List of comma-separated CIDR blocks. If the IP used by the user to connect to the host is different than the address(es) of the host's network interface(s) (for instance, if the address is NAT-ed), then `vault-ssh-helper` cannot authenticate the IP. In these cases, the IP returned by Vault will be matched with the CIDR blocks in this list. If it matches, the authentication succeeds. (Use with caution) 131 | 132 | Sample `config.hcl`: 133 | 134 | ```hcl 135 | vault_addr = "https://vault.example.com:8200" 136 | ssh_mount_point = "ssh" 137 | namespace = "my_namespace" 138 | ca_cert = "/etc/vault-ssh-helper.d/vault.crt" 139 | tls_skip_verify = false 140 | allowed_roles = "*" 141 | ``` 142 | 143 | PAM Configuration 144 | -------------------------------- 145 | Modify the `/etc/pam.d/sshd` file as follows; each option will be explained 146 | below. 147 | 148 | ``` 149 | #@include common-auth 150 | auth requisite pam_exec.so quiet expose_authtok log=/tmp/vaultssh.log /usr/local/bin/vault-ssh-helper -config=/etc/vault-ssh-helper.d/config.hcl 151 | auth optional pam_unix.so not_set_pass use_first_pass nodelay 152 | ``` 153 | 154 | First, the previous authentication mechanism `common-auth`, which is the 155 | standard Linux authentication module, is commented out, in favor of using our 156 | custom configuration. 157 | 158 | Next the authentication configuration for `vault-ssh-helper` is set. 159 | 160 | |Keyword |Description | 161 | |------------------|------------| 162 | |`auth` |PAM type that the configuration applies to. 163 | |`requisite` |If the external command fails, the authentication should fail. 164 | |`pam_exec.so` |PAM module that runs an external command (`vault-ssh-helper`). 165 | |`quiet` |Suppress the exit status of `vault-ssh-helper` from being displayed. 166 | |`expose_authtok` |Binary can read the password from stdin. 167 | |`log` |Path to `vault-ssh-helper`'s log file. 168 | |`vault-ssh-helper`|Absolute path to `vault-ssh-helper`'s binary. 169 | |`config` |The path to `vault-ssh-helper`'s config file. 170 | 171 | The third line works around a bug between some versions of `pam_exec.so` and 172 | `vault-ssh-helper` that causes a successful authentication from 173 | `vault-ssh-helper` to fail due to some resources not being properly released. 174 | Because it is marked as optional, it is essentially a no-op that ensures that 175 | PAM cleans up successfully, avoiding the bug. 176 | 177 | |Option |Description | 178 | |-----------------|------------| 179 | |`auth` |PAM type that the configuration applies to. 180 | |`optional` |If the module fails, authentication does not fail (but if the OTP was invalid, we will have already failed previously). 181 | |`pam_unix.so` |Linux's standard authentication module. 182 | |`not_set_pass` |Module should not be allowed to set or modify passwords. 183 | |`use_first_pass` |Do not display password prompt again. Use the password from the previous module. 184 | |`nodelay` |Avoids the induced delay after entering a wrong password. 185 | 186 | SSHD Configuration 187 | -------------------------------- 188 | Modify the `/etc/ssh/sshd_config` file. Note that for many distributions these 189 | are the default options; you may not need to set them explicitly but should 190 | verify their values if not. 191 | 192 | ``` 193 | ChallengeResponseAuthentication yes 194 | UsePAM yes 195 | PasswordAuthentication no 196 | ``` 197 | 198 | |Option |Description | 199 | |-------------------------------------|------------| 200 | |`ChallengeResponseAuthentication yes`|[Required] Enable challenge response (keyboard-interactive) authentication. 201 | |`UsePAM yes` |[Required] Enable PAM authentication modules. 202 | |`PasswordAuthentication no` |Disable password authentication. 203 | 204 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/vault-ssh-helper 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/hashicorp/go-hclog v1.5.0 7 | github.com/hashicorp/go-uuid v1.0.3 8 | github.com/hashicorp/vault/api v1.10.0 9 | ) 10 | 11 | require ( 12 | github.com/cenkalti/backoff/v3 v3.2.2 // indirect 13 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 14 | github.com/fatih/color v1.15.0 // indirect 15 | github.com/go-jose/go-jose/v3 v3.0.1 // indirect 16 | github.com/go-test/deep v1.1.0 // indirect 17 | github.com/google/go-cmp v0.5.9 // indirect 18 | github.com/hashicorp/errwrap v1.1.0 // indirect 19 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 20 | github.com/hashicorp/go-multierror v1.1.1 // indirect 21 | github.com/hashicorp/go-retryablehttp v0.7.1 // indirect 22 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 23 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect 24 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 25 | github.com/hashicorp/go-sockaddr v1.0.2 // indirect 26 | github.com/hashicorp/hcl v1.0.1-vault-5 // indirect 27 | github.com/mattn/go-colorable v0.1.13 // indirect 28 | github.com/mattn/go-isatty v0.0.17 // indirect 29 | github.com/mitchellh/go-homedir v1.1.0 // indirect 30 | github.com/mitchellh/mapstructure v1.5.0 // indirect 31 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 32 | github.com/ryanuber/go-glob v1.0.0 // indirect 33 | github.com/stretchr/testify v1.8.3 // indirect 34 | golang.org/x/crypto v0.16.0 // indirect 35 | golang.org/x/net v0.19.0 // indirect 36 | golang.org/x/sys v0.15.0 // indirect 37 | golang.org/x/text v0.14.0 // indirect 38 | golang.org/x/time v0.3.0 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 2 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 3 | github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= 4 | github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= 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.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 10 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 11 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 12 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 13 | github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= 14 | github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= 15 | github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= 16 | github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 17 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 18 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 19 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 20 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 21 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 22 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 23 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 24 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 25 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 26 | github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 27 | github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= 28 | github.com/hashicorp/go-hclog v1.5.0/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.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= 33 | github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= 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.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs= 37 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7/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/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 44 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 45 | github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= 46 | github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= 47 | github.com/hashicorp/vault/api v1.10.0 h1:/US7sIjWN6Imp4o/Rj1Ce2Nr5bki/AXi9vAW3p2tOJQ= 48 | github.com/hashicorp/vault/api v1.10.0/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8= 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.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 51 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 52 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 53 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 54 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 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.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 59 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 60 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 61 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 62 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 63 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 64 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 65 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 66 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 67 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 68 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 69 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 70 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 71 | github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 72 | github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= 73 | github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 74 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 75 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 76 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 77 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 78 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 79 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 80 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 81 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 82 | golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 83 | golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= 84 | golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 85 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 86 | golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= 87 | golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= 88 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 89 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 90 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 91 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 94 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 95 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 96 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 97 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 98 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 99 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 100 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 101 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 102 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 103 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 104 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 105 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 106 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 107 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 108 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 109 | -------------------------------------------------------------------------------- /helper/agent.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package helper 5 | 6 | import ( 7 | "fmt" 8 | "net" 9 | "os" 10 | "strings" 11 | 12 | "github.com/hashicorp/go-hclog" 13 | "github.com/hashicorp/vault/api" 14 | ) 15 | 16 | // This allows the testing of the validateIPs function 17 | var netInterfaceAddrs = net.InterfaceAddrs 18 | 19 | // SSHVerifyRequest represents the ssh-helper's verification request. 20 | type SSHVerifyRequest struct { 21 | // Http client to communicate with Vault 22 | Client *api.Client 23 | 24 | // Mount point of SSH backend at Vault 25 | MountPoint string 26 | 27 | // This can be either an echo request message, which if set Vault will 28 | // respond with echo response message. OR, it can be the one-time-password 29 | // entered by the user at the prompt. 30 | OTP string 31 | 32 | // Structure containing configuration parameters of ssh-helper 33 | Config *api.SSHHelperConfig 34 | } 35 | 36 | // VerifyOTP reads the OTP from the prompt and sends the OTP to vault server. Server searches 37 | // for an entry corresponding to the OTP. If there exists one, it responds with the 38 | // IP address and username associated with it. The username returned should match the 39 | // username for which authentication is requested (environment variable PAM_USER holds 40 | // this value). 41 | // 42 | // IP address returned by vault should match the addresses of network interfaces or 43 | // it should belong to the list of allowed CIDR blocks in the config file. 44 | // 45 | // This method is also used to verify if the communication between ssh-helper and Vault 46 | // server can be established with the given configuration data. If OTP in the request 47 | // matches the echo request message, then the echo response message is expected in 48 | // the response, which indicates successful connection establishment. 49 | func VerifyOTP(log hclog.Logger, req *SSHVerifyRequest) error { 50 | // Validating the OTP from Vault server. The response from server can have 51 | // either the response message set OR username and IP set. 52 | resp, err := req.Client.SSHHelperWithMountPoint(req.MountPoint).Verify(req.OTP) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | // If OTP sent was an echo request, look for echo response message in the 58 | // response and return 59 | if req.OTP == api.VerifyEchoRequest { 60 | if resp.Message == api.VerifyEchoResponse { 61 | log.Info("vault-ssh-helper verification successful!") 62 | return nil 63 | } else { 64 | return fmt.Errorf("invalid echo response") 65 | } 66 | } 67 | 68 | // PAM_USER represents the username for which authentication is being 69 | // requested. If the response from vault server mentions the username 70 | // associated with the OTP. It has to be a match. 71 | if resp.Username != os.Getenv("PAM_USER") { 72 | return fmt.Errorf("username mismatch") 73 | } 74 | 75 | // The IP address to which the OTP is associated should be one among 76 | // the network interface addresses of the machine in which helper is 77 | // running. OR it should be present in allowed_cidr_list. 78 | if err := validateIP(resp.IP, req.Config.AllowedCidrList); err != nil { 79 | log.Info(fmt.Sprintf("failed to validate IP: %v", err)) 80 | return err 81 | } 82 | 83 | // If AllowedRoles is `*`, regardless of the rolename returned by the 84 | // Vault server, authentication succeeds. If AllowedRoles is set to 85 | // specific role names, one of these should match the the role name in 86 | // the response for the authentication to succeed. 87 | if err := validateRoleName(log, resp.RoleName, req.Config.AllowedRoles); err != nil { 88 | log.Info(fmt.Sprintf("failed to validate role name: %v", err)) 89 | return err 90 | } 91 | 92 | // Reaching here means that there were no problems. Returning nil will 93 | // gracefully terminate the binary and client will be authenticated to 94 | // establish the session. 95 | log.Info(fmt.Sprintf("%s@%s authenticated!", resp.Username, resp.IP)) 96 | return nil 97 | } 98 | 99 | // Checks if the role name present in the verification response matches 100 | // any of the allowed roles on the helper. 101 | func validateRoleName(log hclog.Logger, respRoleName, allowedRoles string) error { 102 | // Fail the validation when invalid allowed_roles is mentioned 103 | if allowedRoles == "" { 104 | return fmt.Errorf("missing allowed_roles") 105 | } 106 | 107 | // Fastpath to allow any role name 108 | if allowedRoles == "*" { 109 | return nil 110 | } 111 | 112 | respRoleName = strings.TrimSpace(respRoleName) 113 | if respRoleName == "" { 114 | return fmt.Errorf("missing role name in the verification response") 115 | } 116 | 117 | roles := strings.Split(allowedRoles, ",") 118 | log.Info(fmt.Sprintf("roles: %s", roles)) 119 | 120 | for _, role := range roles { 121 | // If an allowed role matches the role name in the response, 122 | // validation succeeds. 123 | if strings.TrimSpace(role) == respRoleName { 124 | return nil 125 | } 126 | } 127 | return fmt.Errorf("role name in the verification response not matching any of the allowed_roles") 128 | } 129 | 130 | // Finds out if given IP address belongs to the IP addresses associated with 131 | // the network interfaces of the machine in which helper is running. 132 | // 133 | // If none of the interface addresses match the given IP, then it is search in 134 | // the comma seperated list of CIDR blocks. This list is supplied as part of 135 | // helper's configuration. 136 | func validateIP(ipStr string, cidrList string) error { 137 | ip := net.ParseIP(ipStr) 138 | 139 | // Scanning network interfaces to find an address match 140 | interfaceAddrs, err := netInterfaceAddrs() 141 | if err != nil { 142 | return err 143 | } 144 | for _, addr := range interfaceAddrs { 145 | var base_addr net.IP 146 | switch ipAddr := addr.(type) { 147 | case *net.IPNet: //IPv4 148 | base_addr = ipAddr.IP 149 | case *net.IPAddr: //IPv6 150 | base_addr = ipAddr.IP 151 | } 152 | if base_addr.String() == ip.String() { 153 | return nil 154 | } 155 | } 156 | 157 | if len(cidrList) == 0 { 158 | return fmt.Errorf("IP did not match any of the network interface addresses. If this was expected, configure the 'allowed_cidr_list' option to allow the IP.") 159 | } 160 | 161 | // None of the network interface addresses matched the given IP. 162 | // Now, try to find a match with the given CIDR blocks. 163 | cidrs := strings.Split(cidrList, ",") 164 | for _, cidr := range cidrs { 165 | belongs, err := belongsToCIDR(ip, cidr) 166 | if err != nil { 167 | return err 168 | } 169 | if belongs { 170 | return nil 171 | } 172 | } 173 | 174 | return fmt.Errorf("invalid IP") 175 | } 176 | 177 | // Checks if the given CIDR block encompasses the given IP address. 178 | func belongsToCIDR(ip net.IP, cidr string) (bool, error) { 179 | _, ipnet, err := net.ParseCIDR(cidr) 180 | if err != nil { 181 | return false, err 182 | } 183 | return ipnet.Contains(ip), nil 184 | } 185 | -------------------------------------------------------------------------------- /helper/agent_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package helper 5 | 6 | import ( 7 | "net" 8 | "testing" 9 | ) 10 | 11 | func TestValidateIP(t *testing.T) { 12 | t.Parallel() 13 | // This allows the testing of the validateIP function 14 | netInterfaceAddrs = func() ([]net.Addr, error) { 15 | var ips []net.Addr 16 | var err error 17 | //var ip net.IP 18 | ips = append(ips, &net.IPNet{IP: net.ParseIP("127.0.0.1"), Mask: net.CIDRMask(8, 32)}) 19 | ips = append(ips, &net.IPNet{IP: net.ParseIP("10.50.100.101"), Mask: net.CIDRMask(24, 32)}) 20 | ips = append(ips, &net.IPNet{IP: net.ParseIP("::1"), Mask: net.CIDRMask(128, 128)}) 21 | 22 | return ips, err 23 | } 24 | var testIP string 25 | var testCIDR string 26 | var err error 27 | 28 | testIP = "10.50.100.101" 29 | testCIDR = "" 30 | err = validateIP(testIP, testCIDR) 31 | // Pass if err == nil 32 | if err != nil { 33 | t.Fatalf("Actual IP Match: expected nill, actual: %s", err) 34 | } 35 | 36 | testIP = "10.50.100.102" 37 | testCIDR = "10.50.100.0/24" 38 | err = validateIP(testIP, testCIDR) 39 | // Pass if err == nil 40 | if err != nil { 41 | t.Fatalf("IP in CIDR: expected nill, actual: %s", err) 42 | } 43 | 44 | testIP = "10.50.100.102" 45 | testCIDR = "" 46 | err = validateIP(testIP, testCIDR) 47 | // Fail if err == nil 48 | if err == nil { 49 | t.Fatalf("IP Does Not Match: expected error, actual: nil") 50 | } 51 | 52 | } 53 | func TestBelongsToCIDR(t *testing.T) { 54 | t.Parallel() 55 | testIP := net.ParseIP("10.50.100.101") 56 | testCIDR := "0.0.0.0/0" 57 | belongs, err := belongsToCIDR(testIP, testCIDR) 58 | if err != nil { 59 | t.Fatalf("err: %s", err) 60 | } 61 | if !belongs { 62 | t.Fatalf("bad: expected:true, actual:false") 63 | } 64 | 65 | testCIDR = "192.168.0.1/16" 66 | belongs, err = belongsToCIDR(testIP, testCIDR) 67 | if err != nil { 68 | t.Fatalf("err: %s", err) 69 | } 70 | if belongs { 71 | t.Fatalf("bad: expected:false, actual:true") 72 | } 73 | 74 | testCIDR = "invalid" 75 | _, err = belongsToCIDR(testIP, testCIDR) 76 | if err == nil { 77 | t.Fatalf("expected error") 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "io/ioutil" 10 | "os" 11 | "strings" 12 | 13 | "github.com/hashicorp/go-hclog" 14 | "github.com/hashicorp/go-uuid" 15 | "github.com/hashicorp/vault-ssh-helper/helper" 16 | "github.com/hashicorp/vault/api" 17 | ) 18 | 19 | // This binary will be run as a command with the goal of client authentication. 20 | // This is not a PAM module per se, but binary fails if verification of OTP 21 | // fails. The PAM configuration runs this binary as an external command via 22 | // the pam_exec.so module as a 'requisite'. Essentially, if this binary fails, 23 | // then the authentication fails. 24 | // 25 | // After the installation and configuration of this helper, verify the installation 26 | // with -verify-only option. 27 | func main() { 28 | log := hclog.Default() 29 | err := Run(log, os.Args[1:]) 30 | if err != nil { 31 | // All the errors are logged using this one statement. All the methods 32 | // simply return appropriate error message. 33 | log.Error(err.Error()) 34 | 35 | // Since this is not a PAM module, exiting with appropriate error 36 | // code does not make sense. Any non-zero exit value is considered 37 | // authentication failure. 38 | os.Exit(1) 39 | } 40 | os.Exit(0) 41 | } 42 | 43 | // Run retrieves OTP from user and validates it with Vault server. Also, if -verify 44 | // option is chosen, an echo request message is sent to Vault instead of OTP. If 45 | // a proper echo message is responded, the verification will be successful. 46 | func Run(log hclog.Logger, args []string) error { 47 | for _, arg := range args { 48 | if arg == "version" || arg == "-v" || arg == "-version" || arg == "--version" { 49 | fmt.Println(formattedVersion()) 50 | return nil 51 | } 52 | } 53 | 54 | var config string 55 | var dev, verifyOnly bool 56 | var logLevel string 57 | flags := flag.NewFlagSet("ssh-helper", flag.ContinueOnError) 58 | flags.StringVar(&config, "config", "", "") 59 | flags.BoolVar(&verifyOnly, "verify-only", false, "") 60 | flags.BoolVar(&dev, "dev", false, "") 61 | flags.StringVar(&logLevel, "log-level", "info", "") 62 | 63 | flags.Usage = func() { 64 | fmt.Printf("%s\n", Help()) 65 | os.Exit(1) 66 | } 67 | 68 | if err := flags.Parse(args); err != nil { 69 | return err 70 | } 71 | 72 | args = flags.Args() 73 | 74 | log.SetLevel(hclog.LevelFromString(logLevel)) 75 | 76 | if len(config) == 0 { 77 | return fmt.Errorf("at least one config path must be specified with -config") 78 | } 79 | 80 | // Load the configuration for this helper 81 | clientConfig, err := api.LoadSSHHelperConfig(config) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | if dev { 87 | log.Warn("Dev mode is enabled!") 88 | if strings.HasPrefix(strings.ToLower(clientConfig.VaultAddr), "https://") { 89 | return fmt.Errorf("unsupported scheme in 'dev' mode") 90 | } 91 | clientConfig.CACert = "" 92 | clientConfig.CAPath = "" 93 | } else if strings.HasPrefix(strings.ToLower(clientConfig.VaultAddr), "http://") { 94 | return fmt.Errorf("unsupported scheme. use 'dev' mode") 95 | } 96 | 97 | // Get an http client to interact with Vault server based on the configuration 98 | client, err := clientConfig.NewClient() 99 | if err != nil { 100 | return err 101 | } 102 | 103 | // Logging namespace and SSH mount point since SSH backend mount point at Vault server 104 | // can vary and helper has no way of knowing these automatically. ssh-helper reads 105 | // the namespace and mount point from the configuration file and uses the same to talk 106 | // to Vault. In case of errors, this can be used for debugging. 107 | // 108 | // If mount point is not mentioned in the config file, default mount point 109 | // of the SSH backend will be used. 110 | log.Info(fmt.Sprintf("using SSH mount point: %s", clientConfig.SSHMountPoint)) 111 | log.Info(fmt.Sprintf("using namespace: %s", clientConfig.Namespace)) 112 | var otp string 113 | if verifyOnly { 114 | otp = api.VerifyEchoRequest 115 | } else { 116 | // Reading the one-time-password from the prompt. This is enabled 117 | // by supplying 'expose_authtok' option to pam module config. 118 | otpBytes, err := ioutil.ReadAll(os.Stdin) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | // Removing the terminator 124 | otp = strings.TrimSuffix(string(otpBytes), string('\x00')) 125 | _, err = uuid.ParseUUID(otp) 126 | if err != nil { 127 | return err 128 | } 129 | } 130 | 131 | // If OTP is echo request, this will be a verify request. Otherwise, this 132 | // will be a OTP validation request. 133 | return helper.VerifyOTP(log, &helper.SSHVerifyRequest{ 134 | Client: client, 135 | MountPoint: clientConfig.SSHMountPoint, 136 | OTP: otp, 137 | Config: clientConfig, 138 | }) 139 | } 140 | 141 | func Help() string { 142 | helpText := ` 143 | Usage: vault-ssh-helper [options] 144 | 145 | vault-ssh-helper takes the One-Time-Password (OTP) from the client and 146 | validates it with Vault server. This binary should be used as an external 147 | command for authenticating clients during for keyboard-interactive auth 148 | of SSH server. 149 | 150 | Options: 151 | 152 | -config= The path on disk to a configuration file. 153 | -dev Run the helper in "dev" mode, (such as testing or http) 154 | -log-level Level of logs to output. Defaults to "info". Supported values are: 155 | "off", "trace", "debug", "info", "warn", and "error". 156 | -verify-only Verify the installation and communication with Vault server 157 | -version Display version. 158 | ` 159 | return strings.TrimSpace(helpText) 160 | } 161 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "io/ioutil" 8 | "os" 9 | "testing" 10 | 11 | "github.com/hashicorp/go-hclog" 12 | "github.com/hashicorp/go-uuid" 13 | "github.com/hashicorp/vault/api" 14 | ) 15 | 16 | func TestVSH_EchoRequestAsOTP(t *testing.T) { 17 | // Check that verify echo request being used as OTP always fails 18 | testRun(t, api.VerifyEchoRequest, false, "uuid string is wrong length") 19 | 20 | // Check that a random non-UUID string is caught 21 | testRun(t, "non-uuid-input", false, "uuid string is wrong length") 22 | 23 | // Passing in a valid UUID should not result in this very same error 24 | uuid, err := uuid.GenerateUUID() 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | testRun(t, uuid, false, "") 29 | } 30 | 31 | func testRun(t *testing.T, otp string, expectSuccess bool, errStr string) { 32 | args := []string{"-config=test-fixtures/config.hcl", "-dev"} 33 | 34 | tempFile, err := ioutil.TempFile("", "test") 35 | if err != nil || tempFile == nil { 36 | t.Fatalf("failed to create temporary file: %v", err) 37 | } 38 | defer tempFile.Close() 39 | 40 | n, err := tempFile.WriteString(otp) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | if n != len(otp) { 46 | t.Fatalf("failed to write otp to temp file") 47 | } 48 | 49 | // Replace stdin for the Run method 50 | os.Stdin = tempFile 51 | 52 | // Reset the offset to the beginning 53 | tempFile.Seek(0, 0) 54 | 55 | err = Run(hclog.Default(), args) 56 | switch { 57 | case expectSuccess: 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | default: 62 | if err == nil { 63 | t.Fatalf("expected an error") 64 | } 65 | if errStr != "" && err.Error() != errStr { 66 | t.Fatalf("expected a different error: got %v expected %v", err, errStr) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | # 6 | # This script builds the application from source for multiple platforms. 7 | set -e 8 | 9 | # Get the parent directory of where this script is. 10 | SOURCE="${BASH_SOURCE[0]}" 11 | while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done 12 | DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" 13 | 14 | # Change into that directory 15 | cd "$DIR" 16 | 17 | # Get the git commit 18 | GIT_COMMIT=$(git rev-parse HEAD) 19 | GIT_DIRTY=$(test -n "`git status --porcelain`" && echo "+CHANGES" || true) 20 | 21 | # Determine the arch/os combos we're building for 22 | XC_ARCH=${XC_ARCH:-"386 amd64 arm"} 23 | XC_OS=${XC_OS:-linux} 24 | 25 | GOPATH=${GOPATH:-$(go env GOPATH)} 26 | case $(uname) in 27 | CYGWIN*) 28 | GOPATH="$(cygpath $GOPATH)" 29 | ;; 30 | esac 31 | 32 | # Delete the old dir 33 | echo "==> Removing old directory..." 34 | rm -f bin/* 35 | rm -rf pkg/* 36 | mkdir -p bin/ 37 | mkdir -p pkg/ 38 | 39 | # If its dev mode, only build for ourself 40 | if [ "${DEV}x" != "x" ]; then 41 | XC_OS=$(go env GOOS) 42 | XC_ARCH=$(go env GOARCH) 43 | fi 44 | 45 | if ! which gox > /dev/null; then 46 | echo "==> Installing gox..." 47 | GO111MODULE=off go get -u github.com/mitchellh/gox 48 | fi 49 | 50 | # Build! 51 | echo "==> Building..." 52 | gox \ 53 | -os="${XC_OS}" \ 54 | -arch="${XC_ARCH}" \ 55 | -ldflags "-X github.com/hashicorp/vault-ssh-helper/main.GitCommit=${GIT_COMMIT}${GIT_DIRTY}" \ 56 | -output "pkg/bin/{{.OS}}_{{.Arch}}/vault-ssh-helper" \ 57 | . 58 | 59 | # Move all the compiled things to the $GOPATH/bin 60 | OLDIFS=$IFS 61 | IFS=: MAIN_GOPATH=($GOPATH) 62 | IFS=$OLDIFS 63 | 64 | # Copy our OS/Arch to the bin/ directory 65 | DEV_PLATFORM="./pkg/bin/$(go env GOOS)_$(go env GOARCH)" 66 | for F in $(find ${DEV_PLATFORM} -mindepth 1 -maxdepth 1 -type f); do 67 | cp ${F} bin/ 68 | cp ${F} ${MAIN_GOPATH}/bin/ 69 | done 70 | 71 | if [ "${DEV}x" = "x" ]; then 72 | # Zip and copy to the dist dir 73 | echo "==> Packaging..." 74 | for PLATFORM in $(find ./pkg/bin -mindepth 1 -maxdepth 1 -type d); do 75 | OSARCH=$(basename ${PLATFORM}) 76 | echo "--> ${OSARCH}" 77 | 78 | pushd $PLATFORM >/dev/null 2>&1 79 | zip ../${OSARCH}.zip ./* 80 | popd >/dev/null 2>&1 81 | done 82 | fi 83 | 84 | # Done! 85 | echo 86 | echo "==> Results:" 87 | ls -hl bin/ 88 | -------------------------------------------------------------------------------- /scripts/cross/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | FROM debian:stable 5 | 6 | RUN apt-get update -y && apt-get install --no-install-recommends -y -q \ 7 | curl \ 8 | zip \ 9 | build-essential \ 10 | ca-certificates \ 11 | git mercurial bzr \ 12 | && rm -rf /var/lib/apt/lists/* 13 | 14 | ENV GOVERSION 1.9.2 15 | RUN mkdir /goroot && mkdir /gopath 16 | RUN curl https://storage.googleapis.com/golang/go${GOVERSION}.linux-amd64.tar.gz \ 17 | | tar xvzf - -C /goroot --strip-components=1 18 | 19 | ENV GOPATH /gopath 20 | ENV GOROOT /goroot 21 | ENV PATH $GOROOT/bin:$GOPATH/bin:$PATH 22 | 23 | RUN go get github.com/mitchellh/gox 24 | 25 | RUN mkdir -p /gopath/src/github.com/hashicorp/vault-ssh-helper 26 | WORKDIR /gopath/src/github.com/hashicorp/vault-ssh-helper 27 | CMD make bin 28 | -------------------------------------------------------------------------------- /scripts/dist.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | set -e 6 | 7 | # Get the version from the command line 8 | VERSION=$1 9 | if [ -z $VERSION ]; then 10 | echo "Please specify a version." 11 | exit 1 12 | fi 13 | 14 | # Make sure we have AWS API keys 15 | if ([ -z $AWS_ACCESS_KEY_ID ] || [ -z $AWS_SECRET_ACCESS_KEY ]) && [ ! -z $HC_RELEASE ]; then 16 | echo "Please set your AWS access key information in the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY env vars." 17 | exit 1 18 | fi 19 | 20 | if [ -z $NOBUILD ] && [ -z $DOCKER_CROSS_IMAGE ]; then 21 | echo "Please set the Docker cross-compile image in DOCKER_CROSS_IMAGE" 22 | exit 1 23 | fi 24 | 25 | # Get the parent directory of where this script is. 26 | SOURCE="${BASH_SOURCE[0]}" 27 | while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done 28 | DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" 29 | 30 | # Change into that dir because we expect that 31 | cd $DIR 32 | 33 | if [ -z $RELBRANCH ]; then 34 | RELBRANCH=master 35 | fi 36 | 37 | # Tag, unless told not to 38 | if [ -z $NOTAG ]; then 39 | echo "==> Tagging..." 40 | git commit --allow-empty --gpg-sign=348FFC4C -m "Cut version $VERSION" 41 | git tag -a -m "Version $VERSION" -s -u 348FFC4C "v${VERSION}" $RELBRANCH 42 | fi 43 | 44 | # Build the packages 45 | if [ -z $NOBUILD ]; then 46 | # This should be a local build of the Dockerfile in the cross dir 47 | docker run --rm -v "$(pwd)":/gopath/src/github.com/hashicorp/vault-ssh-helper -w /gopath/src/github.com/hashicorp/vault-ssh-helper ${DOCKER_CROSS_IMAGE} 48 | fi 49 | 50 | # Zip all the files 51 | rm -rf ./pkg/dist 52 | mkdir -p ./pkg/dist 53 | for FILENAME in $(find ./pkg -mindepth 1 -maxdepth 1 -type f); do 54 | FILENAME=$(basename $FILENAME) 55 | cp ./pkg/${FILENAME} ./pkg/dist/vault-ssh-helper_${VERSION}_${FILENAME} 56 | done 57 | 58 | if [ -z $NOSIGN ]; then 59 | echo "==> Signing..." 60 | pushd ./pkg/dist 61 | rm -f ./vault-ssh-helper_${VERSION}_SHA256SUMS* 62 | shasum -a256 * > ./vault-ssh-helper_${VERSION}_SHA256SUMS 63 | gpg --default-key 348FFC4C --detach-sig ./vault-ssh-helper_${VERSION}_SHA256SUMS 64 | popd 65 | fi 66 | 67 | # Upload 68 | if [ ! -z $HC_RELEASE ]; then 69 | hc-releases upload $DIR/pkg/dist 70 | hc-releases publish 71 | 72 | curl -X PURGE https://releases.hashicorp.com/vault-ssh-helper/${VERSION} 73 | for FILENAME in $(find $DIR/pkg/dist -type f); do 74 | FILENAME=$(basename $FILENAME) 75 | curl -X PURGE https://releases.hashicorp.com/vault-ssh-helper/${VERSION}/${FILENAME} 76 | done 77 | fi 78 | 79 | exit 0 80 | -------------------------------------------------------------------------------- /scripts/update_deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | set -e 7 | 8 | TOOL=vault-ssh-helper 9 | 10 | ## Make a temp dir 11 | tempdir=$(mktemp -d update-${TOOL}-deps.XXXXXX) 12 | 13 | ## Set paths 14 | export GOPATH="$(pwd)/${tempdir}" 15 | export PATH="${GOPATH}/bin:${PATH}" 16 | cd $tempdir 17 | 18 | ## Get Vault 19 | mkdir -p src/github.com/hashicorp 20 | cd src/github.com/hashicorp 21 | echo "Fetching ${TOOL}..." 22 | git clone https://github.com/hashicorp/${TOOL} 23 | cd ${TOOL} 24 | 25 | ## Clean out earlier vendoring 26 | rm -rf Godeps vendor 27 | 28 | ## Get govendor 29 | go get github.com/kardianos/govendor 30 | 31 | ## Init 32 | govendor init 33 | 34 | ## Fetch deps 35 | echo "Fetching deps, will take some time..." 36 | govendor fetch +missing 37 | 38 | echo "Done; to commit run \n\ncd ${GOPATH}/src/github.com/hashicorp/${TOOL}\n" 39 | -------------------------------------------------------------------------------- /test-fixtures/config.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | vault_addr = "http://127.0.0.1:8200" 5 | ssh_mount_point = "ssh" 6 | tls_skip_verify=false 7 | allowed_roles="otp_key_role" 8 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | ) 10 | 11 | var GitCommit string 12 | 13 | const Name = "vault-ssh-helper" 14 | const Version = "0.2.1" 15 | const VersionPrerelease = "" 16 | 17 | // formattedVersion returns a formatted version string which includes the git 18 | // commit and development information. 19 | func formattedVersion() string { 20 | var versionString bytes.Buffer 21 | fmt.Fprintf(&versionString, "%s v%s", Name, Version) 22 | 23 | if VersionPrerelease != "" { 24 | fmt.Fprintf(&versionString, "-%s", VersionPrerelease) 25 | 26 | if GitCommit != "" { 27 | fmt.Fprintf(&versionString, " (%s)", GitCommit) 28 | } 29 | } 30 | return versionString.String() 31 | } 32 | --------------------------------------------------------------------------------