├── .github └── workflows │ └── development.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── backend.go ├── cmd └── vault-plugin-auth-ssh │ └── main.go ├── createsig ├── README.md └── main.go ├── devsetup.sh ├── go.mod ├── go.sum ├── path_config.go ├── path_login.go ├── path_nonce.go ├── path_role.go ├── path_role_test.go ├── pylogin ├── README.md └── login.py ├── ssh.go └── vssh ├── README.md └── main.go /.github/workflows/development.yml: -------------------------------------------------------------------------------- 1 | name: Development 2 | on: [push, pull_request] 3 | jobs: 4 | test-build-upload: 5 | strategy: 6 | matrix: 7 | go-version: [1.23.x] 8 | platform: [ubuntu-latest] 9 | runs-on: ${{ matrix.platform }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | stable: false 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | - name: Test 21 | run: go test ./... 22 | - name: Build 23 | run: | 24 | mkdir -p output/{win,lin,mac} 25 | VERSION=$(git describe --tags) 26 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o output/lin/vault-plugin-auth-ssh cmd/vault-plugin-auth-ssh/main.go 27 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o output/win/vault-plugin-auth-ssh cmd/vault-plugin-auth-ssh/main.go 28 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o output/mac/vault-plugin-auth-ssh cmd/vault-plugin-auth-ssh/main.go 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .vscode 4 | 5 | /vault -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | release: 2 | prerelease: auto 3 | name_template: "{{.ProjectName}} v{{.Version}}" 4 | 5 | builds: 6 | - id: vault-plugin-auth-ssh 7 | main: ./cmd/vault-plugin-auth-ssh 8 | env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - freebsd 12 | - windows 13 | - darwin 14 | - linux 15 | - netbsd 16 | - openbsd 17 | goarch: 18 | - amd64 19 | - arm 20 | - arm64 21 | - 386 22 | goarm: 23 | - 6 24 | - 7 25 | 26 | archives: 27 | - 28 | id: vault-plugin-auth-ssh 29 | builds: 30 | - vault-plugin-auth-ssh 31 | name_template: "{{ .Binary }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 32 | format: binary 33 | files: 34 | - none* 35 | 36 | checksum: 37 | name_template: 'checksums.txt' 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOARCH = amd64 2 | 3 | UNAME = $(shell uname -s) 4 | 5 | ifndef OS 6 | ifeq ($(UNAME), Linux) 7 | OS = linux 8 | else ifeq ($(UNAME), Darwin) 9 | OS = darwin 10 | endif 11 | endif 12 | 13 | .DEFAULT_GOAL := all 14 | 15 | all: fmt build start 16 | 17 | build: 18 | mkdir -p vault/plugins 19 | GOOS=$(OS) GOARCH="$(GOARCH)" go build -o vault/plugins/vault-plugin-auth-ssh cmd/vault-plugin-auth-ssh/main.go 20 | 21 | start: 22 | vault server -dev -dev-root-token-id=root -dev-plugin-dir=./vault/plugins 23 | 24 | enable: 25 | vault auth enable -path=ssh vault-plugin-auth-ssh 26 | 27 | clean: 28 | rm -f ./vault/plugins/vault-plugin-auth-ssh 29 | 30 | fmt: 31 | go fmt $$(go list ./...) 32 | 33 | .PHONY: build clean fmt start enable 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vault Plugin: SSH Auth Backend 2 | 3 | This is a standalone backend plugin for use with [Hashicorp Vault](https://www.github.com/hashicorp/vault). 4 | This plugin allows for SSH public keys and SSH certificates to authenticate with Vault. 5 | 6 | 7 | 8 | - [Vault Plugin: SSH Auth Backend](#vault-plugin-ssh-auth-backend) 9 | - [Getting Started](#getting-started) 10 | - [Usage](#usage) 11 | - [Developing](#developing) 12 | - [Dev setup](#dev-setup) 13 | - [Using the plugin](#using-the-plugin) 14 | - [Global configuration](#global-configuration) 15 | - [ssh_ca_public_keys](#ssh_ca_public_keys) 16 | - [secure_nonce](#secure_nonce) 17 | - [Roles](#roles) 18 | - [SSH certificate](#ssh-certificate) 19 | - [SSH public keys](#ssh-public-keys) 20 | - [Logging in](#logging-in) 21 | - [SSH certificate](#ssh-certificate-1) 22 | - [SSH public key](#ssh-public-key) 23 | - [Using templated policies](#using-templated-policies) 24 | - [Creating signatures](#creating-signatures) 25 | - [Using ssh-agent](#using-ssh-agent) 26 | 27 | 28 | 29 | ## Getting Started 30 | 31 | This is a [Vault plugin](https://www.vaultproject.io/docs/internals/plugins.html) 32 | and is meant to work with Vault. This guide assumes you have already installed Vault 33 | and have a basic understanding of how Vault works. 34 | 35 | Otherwise, first read this guide on how to [get started with Vault](https://www.vaultproject.io/intro/getting-started/install.html). 36 | 37 | To learn specifically about how plugins work, see documentation on [Vault plugins](https://www.vaultproject.io/docs/internals/plugins.html). 38 | 39 | ## Usage 40 | 41 | ```sh 42 | $ vault auth enable -path=ssh vault-plugin-auth-ssh 43 | Success! Enabled vault-plugin-auth-ssh auth method at: ssh/ 44 | ``` 45 | 46 | ## Developing 47 | 48 | If you wish to work on this plugin, you'll first need 49 | [Go](https://www.golang.org) installed on your machine. 50 | 51 | Next, clone this repository into `vault-plugin-auth-ssh`. 52 | 53 | To compile a development version of this plugin, run `make build`. 54 | This will put the plugin binary in the `./vault/plugins` folders. 55 | 56 | Run `make start` to start a development version of vault with this plugin. 57 | 58 | Enable the auth plugin backend using the SSH auth plugin: 59 | 60 | ```sh 61 | $ vault auth enable -path=ssh vault-plugin-auth-ssh 62 | Success! Enabled vault-plugin-auth-ssh auth method at: ssh/ 63 | ``` 64 | 65 | ### Dev setup 66 | 67 | Look into the `devsetup.sh` script, this will build the plugin, build certsig and setup a test environment with ssh-client signing, ssh certificate and public key test. 68 | 69 | ## Using the plugin 70 | 71 | ### Global configuration 72 | 73 | You may optionally set any of the 74 | [standard Vault token parameters](https://pkg.go.dev/github.com/hashicorp/vault/sdk/helper/tokenutil#TokenParams) 75 | which will be used as defaults if not overridden in a role. 76 | 77 | #### ssh_ca_public_keys 78 | 79 | If you want to use ssh certificates you'll need to configure the ssh CA's which the certificates will validate against. 80 | 81 | sshca in this example is a file containing your SSH CA. You can specify multiple CA's. 82 | 83 | ```sh 84 | $ vault write auth/ssh/config ssh_ca_public_keys=@sshca 85 | 86 | $ vault read auth/ssh/config 87 | Key Value 88 | --- ----- 89 | secure_nonce true 90 | ssh_ca_public_keys [ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDrLFN/LCDOjPw327hWfHXMOk9+GmP+pOl2JEG7eSfkzwVhumDU12swjnPQ9H1tZVWzcfufTg+PgMd/hP19ADkRxQ2CTbz7YUPdD6LvJOCRK8TK+tKliaFL9/lWFtlitERyk91ZSqGbROjtCyGlnetxY1+tF5NqLFtQ1tsPrxjjdRQoUMHlF8yv/VUxMOCjAmuqxKrEl5mfZJcnYpnfEBgWoZNTKAXkp6KJWLAxyiHPVTt7azyMzivCTZc8eCKXIInRpOMR7TvHGxPG8tHn2XrI01ni9zXQ+xG1sqxecPBSWU8fekKxwg5bikrWw4/9kCNvxrwBpf1IzlIKhugig8MP3+Jlrjp5BFXFuaQatIk6zLMkzDpE/iZwDZv5qicXdLK/nbKHmGqFWupcvfHUe6rh16TOYFbpnRMOEvTYpR/PfLlnQKcbkQgbDR01N8DfLetxt635C+ANU4N1ebQqjKkwb8ZPr2ryF/Y8Z1PV0x5H25r8UZyoGAXIsP3zkP0Ev40Bx3umlU/jR8nF6QQmXdbs2McfZFO2g0VsXSzUOR0L5s5Sd/uoUCcpz9nmKlgRIqHIhVGF3+FjrIaj3tXT7ucyPAsVVk/l4yhMQSuNtFi0eqZRPcdMiKff5W9PfVyEkpXTcSFweGPdVehZxPnM7DfH7axpg73OLWxvwVzkah31WQ==] 91 | token_bound_cidrs [] 92 | token_explicit_max_ttl 0s 93 | token_max_ttl 0s 94 | token_no_default_policy false 95 | token_num_uses 0 96 | token_period 0s 97 | token_policies [] 98 | token_ttl 0s 99 | token_type default 100 | ``` 101 | 102 | #### secure_nonce 103 | 104 | By default the `secure_nonce` config option is enabled. 105 | This means vault will generate a nonce for you with a lifetime of 30 seconds. 106 | You can get this nonce by doing `vault read auth/ssh/nonce` the resulting nonce must be used to create a signature over. (see [Creating signatures](#creating-signatures)) 107 | 108 | If you disable `secure_nonce` you can use timebased nonces. (this is a possible security risk) 109 | 110 | ### Roles 111 | 112 | You can create / list / delete roles which are used to link your certificate or public keys to Vault policies. 113 | 114 | You may optionally set any of the 115 | [standard Vault token parameters](https://pkg.go.dev/github.com/hashicorp/vault/sdk/helper/tokenutil#TokenParams) 116 | to override default values. 117 | 118 | #### SSH certificate 119 | 120 | Create a role with the policy `ssh-policy` bound to a certificate with the principal `ubuntu`. 121 | (prerequisite: a SSH CA needs to be configured in auth/ssh/config) 122 | 123 | ```sh 124 | $ vault write auth/ssh/role/ubuntu token_policies="ssh-policy" principals="ubuntu" 125 | 126 | $ vault read auth/ssh/role/ubuntu 127 | Key Value 128 | --- ----- 129 | principals [ubuntu] 130 | public_keys 131 | token_bound_cidrs [] 132 | token_explicit_max_ttl 0s 133 | token_max_ttl 0s 134 | token_no_default_policy false 135 | token_num_uses 0 136 | token_period 0s 137 | token_policies [ssh-policy] 138 | token_ttl 0s 139 | token_type default 140 | ``` 141 | 142 | #### SSH public keys 143 | 144 | Create a role with the policy `ssh-policy` bound to a specific publickey. 145 | 146 | ```sh 147 | $ vault write auth/ssh/role/ubuntu token_policies="ssh-policy" public_keys=@sshkey.pub 148 | 149 | $ vault read auth/ssh/role/ubuntu 150 | Key Value 151 | --- ----- 152 | principals 153 | public_keys [ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGg0xzFrvEYbZGkF5vWlHUutACUTLH7WMUG09NOi6skL] 154 | token_bound_cidrs [] 155 | token_explicit_max_ttl 0s 156 | token_max_ttl 0s 157 | token_no_default_policy false 158 | token_num_uses 0 159 | token_period 0s 160 | token_policies [ssh-policy] 161 | token_ttl 0s 162 | token_type default 163 | ``` 164 | 165 | #### SSH public key self-registration 166 | 167 | If your Vault has another authentication method that users can use, 168 | perhaps one that requires more user interaction such as 169 | [OIDC authentication](https://learn.hashicorp.com/tutorials/vault/oidc-auth), 170 | it is possible to use that other authentication method to securely 171 | enable users to register their own public keys for SSH authentication. 172 | 173 | First, configure default standard token parameters for SSH 174 | authentication as above, so users do not have to choose them, 175 | especially the `token_policies` parameter. 176 | Next, configure the other authentication method with a policy that 177 | enables writing public keys to the `auth/ssh/role` path but doesn't 178 | allow other parameters. 179 | For example use a 180 | [templated policy](https://www.vaultproject.io/docs/concepts/policies#templated-policies) 181 | like this: 182 | ``` 183 | path "auth/ssh/role/{{identity.entity.aliases..name}}" { 184 | capabilities = ["create", "update"] 185 | denied_parameters = { 186 | "public_keys" = ["@*"] 187 | } 188 | allowed_parameters = { 189 | "public_keys" = [] 190 | } 191 | } 192 | ``` 193 | 194 | ### Logging in 195 | 196 | #### SSH certificate 197 | 198 | ```sh 199 | nonce=$(vault read -field nonce auth/ssh/nonce) 200 | vault write auth/ssh/login role= cert=@ nonce=$nonce signature= 201 | ``` 202 | 203 | For example 204 | 205 | ```sh 206 | vault write auth/ssh/login role=ubuntu cert=@id_rsa-cert.pub signature=7ou2bupUMNmMqcorurOnbKnpbh9Kc7aBrF7nk6li0AhgnYAzhgfgGB3qJqI4qmf9TIc/x3JoNzo+Xq7KqXOXCA== nonce=AQAAAA7XdbPVKJn3uwA8 207 | 208 | Key Value 209 | --- ----- 210 | token s.TpHP2eRCZNhuOENZUMas6YmV 211 | token_accessor 7FFAcAnxKOyM8BYWEN2KfYzR 212 | token_duration 768h 213 | token_renewable true 214 | token_policies ["default" "ssh-policy"] 215 | identity_policies [] 216 | policies ["default" "ssh-policy"] 217 | token_meta_role ubuntu 218 | ``` 219 | 220 | See [Creating signatures](#creating-signatures) about how to create those. 221 | 222 | #### SSH public key 223 | 224 | ```sh 225 | nonce=$(vault read -field nonce auth/ssh/nonce) 226 | vault write auth/ssh/login role= public_key=@ nonce=$nonce signature= 227 | ``` 228 | 229 | For example 230 | 231 | ```sh 232 | $ vault write auth/ssh/login role=ubuntu public_key=@id_rsa.pub signature=fiEHdDHClYJIlRNWMC6c5QpM3ePi1xJh1KB90NI7CedZh0Siya5SG8ohy6zOk7e5l8Mdhx/FelykL43KH+OwBw== nonce=AQAAAA7XdbTAIytAhQA8 233 | 234 | Key Value 235 | --- ----- 236 | token s.8L1uONTtaLHzZEtNw2oKmurk 237 | token_accessor SQ6jWOYKblSBFqywTezcdqVk 238 | token_duration 768h 239 | token_renewable true 240 | token_policies ["default" "ssh-policy"] 241 | identity_policies [] 242 | policies ["default" "ssh-policy"] 243 | token_meta_role ubuntu 244 | ``` 245 | 246 | See [Creating signatures](#creating-signatures) about how to create those. 247 | 248 | #### Using templated policies 249 | 250 | This plugin makes aliases available for use in Vault 251 | [templated policies](https://www.vaultproject.io/docs/concepts/policies#templated-policies). 252 | These can be used to limit what secrets a policy makes available while 253 | sharing one policy between multiple roles. 254 | The defined role is always available in policies as 255 | `{{identity.entity.aliases..name}}`. 256 | In addition, a login can add any metadata keys with values to further 257 | limit secrets paths via the `metadata` parameter available as 258 | `{{identity.entity.aliases..metadata.}}`. 259 | The metadata parameter is a mapping of keys to values which should be 260 | input as JSON, for example: 261 | 262 | For example 263 | 264 | ```sh 265 | echo '{ "metadata": { "key1" : "val1", "key2": "val2" } }' | \ 266 | vault write auth/ssh/login role= public_key=@ nonce= signature= - 267 | ``` 268 | 269 | will create the metadata keys "key1" and key2" with values "val1" and 270 | "val2", respectively. 271 | 272 | ### Creating signatures 273 | 274 | For now you can use the [createsig](createsig/README.md) tool to generate your signature and nonce. 275 | 276 | ```text 277 | This tool will print out a signature based on a nonce to be used with vault-plugin-auth-ssh 278 | 279 | Need createsig 280 | eg. createsig 5f780af8-75ff-f209-cd31-500879e18640 id_rsa mypassword 281 | 282 | If you don't have a password just omit it 283 | eg. createsig 5f780af8-75ff-f209-cd31-500879e18640 id_rsa 284 | ``` 285 | 286 | You must get the nonce from `vault read auth/ssh/nonce` 287 | 288 | For example: 289 | 290 | ```sh 291 | $ vault write auth/ssh/login role=ubuntu public_key=@id_rsa.pub $(createsig $(vault read -field nonce auth/ssh/nonce) vaultid_rsa) 292 | ``` 293 | 294 | ```sh 295 | $ vault write auth/ssh/login role=ubuntu cert=@id_rsa-cert.pub $(createsig $(vault read -field nonce auth/ssh/nonce) id_rsa) 296 | ``` 297 | 298 | With a pass 299 | 300 | ```sh 301 | $ vault write auth/ssh/login role=ubuntu public_key=@id_rsa.pub $(createsig $(vault read -field nonce auth/ssh/nonce) id_rsa yourpass) 302 | ``` 303 | 304 | ### Using ssh-agent 305 | 306 | Signatures can also be created using ssh-agent. 307 | See the [vssh README](vssh/README.md) and [pylogin README](pylogin/README.md) 308 | for examples of how to do that. 309 | 310 | ### Running in Containerized Environments (Docker, Kubernetes, etc) 311 | 312 | Vault will attempt to lock its memory to avoid swapping unencrypted secrets to disk. 313 | 314 | This is explained in some detail here: [https://support.hashicorp.com/hc/en-us/articles/115012787688-Vault-and-mlock](https://support.hashicorp.com/hc/en-us/articles/115012787688-Vault-and-mlock) 315 | 316 | When running in Kubernetes, this is achieved by setting the _securityContext_ in the container spec by adding the following to the container definition: 317 | 318 | securityContext: 319 | capabilities: 320 | add: 321 | - IPC_LOCK 322 | 323 | However, this only gives the ability of a file to use mlock through Kubernetes. The executable files themselves need this to be set as well. 324 | 325 | The Vault binary itself is distributed with the capabilities (which are stored via extensions on the file itself) enabled for Vault to use mlock. Any _plugins_ however, must have this set as well to work outside of "dev mode" (which disables mlock). 326 | 327 | You can do this in your Dockerfile when creating your own images, or at any other point before Vault attempts to use the plugin. 328 | 329 | When creating container images using Vault plugins like this one, be sure to add the following steps to your Dockerfile: 330 | 331 | RUN chown vault:vault /path/to/vault-plugins/vault-plugin-auth-ssh 332 | 333 | RUN chmod 700 /path/to/vault-plugins/vault-plugin-auth-ssh 334 | 335 | RUN setcap cap_ipc_lock=+ep /path/to/vault-plugins/vault-plugin-auth-ssh 336 | 337 | Setting the owner/group and mode will prevent problems when using setting `VAULT_ENABLE_FILE_PERMISSIONS_CHECK` to true. 338 | 339 | Running the final `setcap` command will allow the plugin to function when Vault is using mlock. -------------------------------------------------------------------------------- /backend.go: -------------------------------------------------------------------------------- 1 | package sshauth 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "github.com/hashicorp/errwrap" 12 | "github.com/hashicorp/vault/sdk/framework" 13 | "github.com/hashicorp/vault/sdk/helper/tokenutil" 14 | "github.com/hashicorp/vault/sdk/logical" 15 | ) 16 | 17 | const ( 18 | rolePrefix string = "role/" 19 | ) 20 | 21 | // backend wraps the backend framework and adds a map for storing key value pairs. 22 | type backend struct { 23 | *framework.Backend 24 | nonces map[string]time.Time 25 | nonceLock sync.RWMutex 26 | } 27 | 28 | type ConfigEntry struct { 29 | tokenutil.TokenParams 30 | 31 | SSHCAPublicKeys []string `json:"ssh_ca_public_keys"` 32 | SecureNonce bool `json:"secure_nonce"` 33 | } 34 | 35 | var _ logical.Factory = Factory 36 | 37 | // Factory configures and returns Mock backends 38 | func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { 39 | b := newBackend() 40 | 41 | if conf == nil { 42 | return nil, fmt.Errorf("configuration passed into backend is nil") 43 | } 44 | 45 | if err := b.Setup(ctx, conf); err != nil { 46 | return nil, err 47 | } 48 | 49 | return b, nil 50 | } 51 | 52 | func newBackend() *backend { 53 | b := &backend{} 54 | 55 | b.Backend = &framework.Backend{ 56 | Help: strings.TrimSpace(backendHelp), 57 | BackendType: logical.TypeCredential, 58 | AuthRenew: b.pathAuthRenew, 59 | PathsSpecial: &logical.Paths{ 60 | Unauthenticated: []string{ 61 | "login", 62 | "nonce", 63 | }, 64 | SealWrapStorage: []string{ 65 | "config", 66 | }, 67 | }, 68 | Paths: framework.PathAppend( 69 | []*framework.Path{ 70 | b.pathLogin(), 71 | b.pathNonce(), 72 | b.pathConfig(), 73 | b.pathRoleList(), 74 | b.pathRole(), 75 | }, 76 | ), 77 | PeriodicFunc: b.periodicFunc, 78 | } 79 | 80 | b.nonces = make(map[string]time.Time) 81 | 82 | return b 83 | } 84 | 85 | func (b *backend) pathAuthRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 86 | roleName := req.Auth.InternalData["role"].(string) 87 | if roleName == "" { 88 | return nil, errors.New("failed to fetch role_name during renewal") 89 | } 90 | 91 | // Ensure that the Role still exists. 92 | role, err := b.role(ctx, req.Storage, roleName) 93 | if err != nil { 94 | return nil, errwrap.Wrapf(fmt.Sprintf("failed to validate role %s during renewal: {{err}}", roleName), err) 95 | } 96 | 97 | if role == nil { 98 | return nil, fmt.Errorf("role %s does not exist during renewal", roleName) 99 | } 100 | 101 | resp := &logical.Response{ 102 | Auth: req.Auth, 103 | } 104 | 105 | resp.Auth.TTL = role.TokenTTL 106 | resp.Auth.MaxTTL = role.TokenMaxTTL 107 | resp.Auth.Period = role.TokenPeriod 108 | 109 | return resp, nil 110 | } 111 | 112 | func (b *backend) periodicFunc(ctx context.Context, req *logical.Request) error { 113 | b.nonceCleanup() 114 | 115 | return nil 116 | } 117 | 118 | const ( 119 | backendHelp = ` 120 | The SSH backend plugin allows authentication using SSH certificates and public keys. 121 | ` 122 | ) 123 | -------------------------------------------------------------------------------- /cmd/vault-plugin-auth-ssh/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | sshauth "github.com/42wim/vault-plugin-auth-ssh" 7 | "github.com/hashicorp/go-hclog" 8 | "github.com/hashicorp/vault/api" 9 | "github.com/hashicorp/vault/sdk/plugin" 10 | ) 11 | 12 | func main() { 13 | apiClientMeta := &api.PluginAPIClientMeta{} 14 | flags := apiClientMeta.FlagSet() 15 | flags.Parse(os.Args[1:]) 16 | 17 | tlsConfig := apiClientMeta.GetTLSConfig() 18 | tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig) 19 | 20 | err := plugin.Serve(&plugin.ServeOpts{ 21 | BackendFactoryFunc: sshauth.Factory, 22 | TLSProviderFunc: tlsProviderFunc, 23 | }) 24 | if err != nil { 25 | logger := hclog.New(&hclog.LoggerOptions{}) 26 | 27 | logger.Error("plugin shutting down", "error", err) 28 | os.Exit(1) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /createsig/README.md: -------------------------------------------------------------------------------- 1 | # Createsig 2 | 3 | ```text 4 | This tool will print out a signature and nonce to be used with vault-plugin-auth-ssh 5 | 6 | Need createsig 7 | eg. createsig id_rsa mypassword 8 | 9 | If you don't have a password just omit it 10 | eg. createsig id_rsa 11 | ``` 12 | 13 | ## building 14 | 15 | Use `go get`, resulting binary will be in `~/go/bin/createsig` 16 | 17 | ```sh 18 | go get github.com/42wim/vault-plugin-auth-ssh/createsig 19 | ``` 20 | -------------------------------------------------------------------------------- /createsig/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "time" 11 | 12 | "golang.org/x/crypto/ssh" 13 | ) 14 | 15 | func genSig(nonce, privatekey, password string) { 16 | var ( 17 | signer ssh.Signer 18 | err error 19 | ) 20 | 21 | pemBytes, err := ioutil.ReadFile(privatekey) 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | 26 | if password == "" { 27 | signer, err = ssh.ParsePrivateKey(pemBytes) 28 | if err != nil { 29 | log.Fatalf("parse key failed:%v", err) 30 | } 31 | } else { 32 | signer, err = ssh.ParsePrivateKeyWithPassphrase(pemBytes, []byte(password)) 33 | } 34 | 35 | var signBytes []byte 36 | 37 | if nonce == "" { 38 | t := time.Now() 39 | timeBytes, _ := t.MarshalBinary() 40 | signBytes = append(signBytes, timeBytes...) 41 | } else { 42 | signBytes = append(signBytes, []byte(nonce)...) 43 | } 44 | 45 | res, _ := signer.Sign(rand.Reader, signBytes) 46 | 47 | signatureBlob := res.Blob 48 | 49 | fmt.Println("signature=" + base64.StdEncoding.EncodeToString(signatureBlob) + " nonce=" + base64.StdEncoding.EncodeToString(signBytes)) 50 | } 51 | 52 | func printHelp() { 53 | fmt.Println("This tool will print out a signature based on a nonce to be used with vault-plugin-auth-ssh") 54 | fmt.Println("You can get a nonce by running \"vault read auth/ssh/nonce\"") 55 | fmt.Println("") 56 | fmt.Println("Need " + os.Args[0] + " ") 57 | fmt.Println("eg. " + os.Args[0] + " anonce ~/.ssh/id_rsa mypassword") 58 | fmt.Println("") 59 | fmt.Println("If you don't have a password just omit it") 60 | fmt.Println("eg. " + os.Args[0] + " anonce ~/.ssh/id_rsa") 61 | } 62 | 63 | func main() { 64 | switch len(os.Args) { 65 | case 2: 66 | genSig(os.Args[1], "", "") 67 | case 3: 68 | genSig(os.Args[1], os.Args[2], "") 69 | case 4: 70 | genSig(os.Args[1], os.Args[2], os.Args[3]) 71 | default: 72 | printHelp() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /devsetup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export VAULT_ADDR="http://127.0.0.1:8200" 3 | mkdir tmp 4 | mkdir -p vault/plugins 5 | 6 | set -e 7 | echo "generating sshkey" 8 | rm -f tmp/sshkey 9 | ssh-keygen -t ed25519 -f tmp/sshkey -N "" 10 | 11 | echo "building plugin" 12 | go build -o vault/plugins/vault-plugin-auth-ssh cmd/vault-plugin-auth-ssh/main.go 13 | 14 | echo "building createsig" 15 | go build -o createsig/createsig createsig/main.go 16 | 17 | vault server -dev -dev-root-token-id=root -dev-plugin-dir=./vault/plugins & 18 | sleep 5 19 | 20 | vault login root 21 | vault secrets enable -path=ssh-client-signer ssh 22 | vault write -field=public_key ssh-client-signer/config/ca generate_signing_key=true >tmp/sshca 23 | vault write ssh-client-signer/roles/my-role - <<"EOH" 24 | { 25 | "allow_user_certificates": true, 26 | "allowed_users": "*", 27 | "allowed_extensions": "permit-pty,permit-port-forwarding", 28 | "default_extensions": [ 29 | { 30 | "permit-pty": "" 31 | } 32 | ], 33 | "key_type": "ca", 34 | "default_user": "ubuntu", 35 | "ttl": "30m0s" 36 | } 37 | EOH 38 | vault write -field=signed_key ssh-client-signer/sign/my-role public_key=@tmp/sshkey.pub >tmp/sshkey-cert.pub 39 | vault auth enable -path=ssh vault-plugin-auth-ssh 40 | vault write auth/ssh/config ssh_ca_public_keys=@tmp/sshca 41 | vault write auth/ssh/role/ubuntu token_policies="ssh-policy" principals="ubuntu" 42 | vault write auth/ssh/role/ubuntu2 token_policies="ssh-policy" public_keys=@tmp/sshkey.pub 43 | 44 | echo "" 45 | echo "" 46 | echo "You can now login with certificate via:" 47 | echo "vault write auth/ssh/login role=ubuntu cert=@tmp/sshkey-cert.pub $(createsig/createsig $(vault read -field nonce auth/ssh/nonce) tmp/sshkey)" 48 | echo "" 49 | echo "" 50 | echo "You can now login with publickey via:" 51 | echo "vault write auth/ssh/login role=ubuntu2 public_key=@tmp/sshkey.pub $(createsig/createsig $(vault read -field nonce auth/ssh/nonce) tmp/sshkey)" 52 | echo "" 53 | echo "" 54 | echo "run killall -9 vault-plugin-auth-ssh && killall -9 vault to kill running dev" 55 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/42wim/vault-plugin-auth-ssh 2 | 3 | go 1.23.3 4 | 5 | require ( 6 | github.com/hashicorp/errwrap v1.1.0 7 | github.com/hashicorp/go-hclog v1.6.3 8 | github.com/hashicorp/go-sockaddr v1.0.7 9 | github.com/hashicorp/go-uuid v1.0.3 10 | github.com/hashicorp/vault/api v1.16.0 11 | github.com/hashicorp/vault/sdk v0.15.2 12 | golang.org/x/crypto v0.36.0 13 | ) 14 | 15 | require ( 16 | github.com/Microsoft/go-winio v0.6.2 // indirect 17 | github.com/armon/go-metrics v0.4.1 // indirect 18 | github.com/armon/go-radix v1.0.0 // indirect 19 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 20 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 21 | github.com/distribution/reference v0.6.0 // indirect 22 | github.com/docker/docker v27.2.1+incompatible // indirect 23 | github.com/docker/go-connections v0.5.0 // indirect 24 | github.com/docker/go-units v0.5.0 // indirect 25 | github.com/evanphx/json-patch/v5 v5.6.0 // indirect 26 | github.com/fatih/color v1.17.0 // indirect 27 | github.com/felixge/httpsnoop v1.0.4 // indirect 28 | github.com/go-jose/go-jose/v4 v4.0.4 // indirect 29 | github.com/go-logr/logr v1.4.2 // indirect 30 | github.com/go-logr/stdr v1.2.2 // indirect 31 | github.com/gogo/protobuf v1.3.2 // indirect 32 | github.com/golang/protobuf v1.5.4 // indirect 33 | github.com/golang/snappy v0.0.4 // indirect 34 | github.com/google/certificate-transparency-go v1.3.1 // indirect 35 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 36 | github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6 // indirect 37 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 38 | github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.1 // indirect 39 | github.com/hashicorp/go-kms-wrapping/v2 v2.0.18 // indirect 40 | github.com/hashicorp/go-multierror v1.1.1 // indirect 41 | github.com/hashicorp/go-plugin v1.6.1 // indirect 42 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 43 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 44 | github.com/hashicorp/go-secure-stdlib/cryptoutil v0.1.1 // indirect 45 | github.com/hashicorp/go-secure-stdlib/mlock v0.1.3 // indirect 46 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9 // indirect 47 | github.com/hashicorp/go-secure-stdlib/permitpool v1.0.0 // indirect 48 | github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.1 // indirect 49 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 50 | github.com/hashicorp/go-version v1.7.0 // indirect 51 | github.com/hashicorp/golang-lru v1.0.2 // indirect 52 | github.com/hashicorp/hcl v1.0.1-vault-5 // indirect 53 | github.com/hashicorp/yamux v0.1.1 // indirect 54 | github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 // indirect 55 | github.com/mattn/go-colorable v0.1.13 // indirect 56 | github.com/mattn/go-isatty v0.0.20 // indirect 57 | github.com/mitchellh/copystructure v1.2.0 // indirect 58 | github.com/mitchellh/go-homedir v1.1.0 // indirect 59 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 60 | github.com/mitchellh/mapstructure v1.5.0 // indirect 61 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 62 | github.com/moby/docker-image-spec v1.3.1 // indirect 63 | github.com/oklog/run v1.1.0 // indirect 64 | github.com/opencontainers/go-digest v1.0.0 // indirect 65 | github.com/opencontainers/image-spec v1.1.0 // indirect 66 | github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect 67 | github.com/pierrec/lz4 v2.6.1+incompatible // indirect 68 | github.com/pkg/errors v0.9.1 // indirect 69 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 70 | github.com/robfig/cron/v3 v3.0.1 // indirect 71 | github.com/ryanuber/go-glob v1.0.0 // indirect 72 | github.com/sasha-s/go-deadlock v0.3.5 // indirect 73 | github.com/stretchr/testify v1.10.0 // indirect 74 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect 75 | go.opentelemetry.io/otel v1.31.0 // indirect 76 | go.opentelemetry.io/otel/metric v1.31.0 // indirect 77 | go.opentelemetry.io/otel/trace v1.31.0 // indirect 78 | go.uber.org/atomic v1.11.0 // indirect 79 | golang.org/x/net v0.34.0 // indirect 80 | golang.org/x/sys v0.31.0 // indirect 81 | golang.org/x/text v0.23.0 // indirect 82 | golang.org/x/time v0.9.0 // indirect 83 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect 84 | google.golang.org/grpc v1.69.4 // indirect 85 | google.golang.org/protobuf v1.36.3 // indirect 86 | gopkg.in/yaml.v3 v3.0.1 // indirect 87 | ) 88 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= 2 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 3 | github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= 4 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 5 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 6 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 7 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 8 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 9 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 10 | github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= 11 | github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= 12 | github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= 13 | github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 14 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 15 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 16 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 17 | github.com/bufbuild/protocompile v0.10.0 h1:+jW/wnLMLxaCEG8AX9lD0bQ5v9h1RUiMKOBOT5ll9dM= 18 | github.com/bufbuild/protocompile v0.10.0/go.mod h1:G9qQIQo0xZ6Uyj6CMNz0saGmx2so+KONo8/KrELABiY= 19 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 20 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 21 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 22 | github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= 23 | github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= 24 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 25 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 26 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 29 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 30 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 31 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 32 | github.com/docker/docker v27.2.1+incompatible h1:fQdiLfW7VLscyoeYEBz7/J8soYFDZV1u6VW6gJEjNMI= 33 | github.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 34 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 35 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 36 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 37 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 38 | github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= 39 | github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= 40 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 41 | github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 42 | github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 43 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 44 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 45 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 46 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 47 | github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= 48 | github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= 49 | github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= 50 | github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= 51 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 52 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 53 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 54 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 55 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 56 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 57 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 58 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 59 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 60 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 61 | github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= 62 | github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 63 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 64 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 65 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 66 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 67 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 68 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 69 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 70 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 71 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 72 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 73 | github.com/google/certificate-transparency-go v1.3.1 h1:akbcTfQg0iZlANZLn0L9xOeWtyCIdeoYhKrqi5iH3Go= 74 | github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k= 75 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 76 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 77 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 78 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 79 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 80 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 81 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 82 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= 83 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= 84 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= 85 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 86 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 87 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 88 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 89 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 90 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 91 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 92 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 93 | github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6 h1:kBoJV4Xl5FLtBfnBjDvBxeNSy2IRITSGs73HQsFUEjY= 94 | github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6/go.mod h1:y+HSOcOGB48PkUxNyLAiCiY6rEENu+E+Ss4LG8QHwf4= 95 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 96 | github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= 97 | github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 98 | github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.1 h1:KIge4FHZEDb2/xjaWgmBheCTgRL6HV4sgTfDsH876L8= 99 | github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.1/go.mod h1:aHO1EoFD0kBYLBedqxXgalfFT8lrWfP7kpuSoaqGjH0= 100 | github.com/hashicorp/go-kms-wrapping/v2 v2.0.18 h1:DLfC677GfKEpSAFpEWvl1vXsGpEcSHmbhBaPLrdDQHc= 101 | github.com/hashicorp/go-kms-wrapping/v2 v2.0.18/go.mod h1:t/eaR/mi2mw3klfl1WEAuiLKrlZ/Q8cosmsT+RIPLu0= 102 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 103 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 104 | github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI= 105 | github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= 106 | github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= 107 | github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= 108 | github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= 109 | github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= 110 | github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= 111 | github.com/hashicorp/go-secure-stdlib/cryptoutil v0.1.1 h1:VaLXp47MqD1Y2K6QVrA9RooQiPyCgAbnfeJg44wKuJk= 112 | github.com/hashicorp/go-secure-stdlib/cryptoutil v0.1.1/go.mod h1:hH8rgXHh9fPSDPerG6WzABHsHF+9ZpLhRI1LPk4JZ8c= 113 | github.com/hashicorp/go-secure-stdlib/mlock v0.1.3 h1:kH3Rhiht36xhAfhuHyWJDgdXXEx9IIZhDGRk24CDhzg= 114 | github.com/hashicorp/go-secure-stdlib/mlock v0.1.3/go.mod h1:ov1Q0oEDjC3+A4BwsG2YdKltrmEw8sf9Pau4V9JQ4Vo= 115 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9 h1:FW0YttEnUNDJ2WL9XcrrfteS1xW8u+sh4ggM8pN5isQ= 116 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= 117 | github.com/hashicorp/go-secure-stdlib/permitpool v1.0.0 h1:U6y5MXGiDVOOtkWJ6o/tu1TxABnI0yKTQWJr7z6BpNk= 118 | github.com/hashicorp/go-secure-stdlib/permitpool v1.0.0/go.mod h1:ecDb3o+8D4xtP0nTCufJaAVawHavy5M2eZ64Nq/8/LM= 119 | github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.1 h1:JY+zGg8gOmslwif1fiCqT5Hu1SikLZQcHkmQhCoA9gY= 120 | github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.1/go.mod h1:jW3KCTvdPyAdVecOUwiiO2XaYgUJ/isigt++ISkszkY= 121 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= 122 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= 123 | github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= 124 | github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= 125 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 126 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 127 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 128 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 129 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 130 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 131 | github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 132 | github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 133 | github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= 134 | github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= 135 | github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4= 136 | github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA= 137 | github.com/hashicorp/vault/sdk v0.15.2 h1:Rp5Yp4lyBhlWgq24ZVb2n/YN47RKOAvmx/jlMfS9ku4= 138 | github.com/hashicorp/vault/sdk v0.15.2/go.mod h1:2Wj2tHIgfz0gNWgEPWBbCXFIiPrq96E8FTjPNV9J1Bc= 139 | github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= 140 | github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= 141 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 142 | github.com/jhump/protoreflect v1.16.0 h1:54fZg+49widqXYQ0b+usAFHbMkBGR4PpXrsHc8+TBDg= 143 | github.com/jhump/protoreflect v1.16.0/go.mod h1:oYPd7nPvcBw/5wlDfm/AVmU9zH9BgqGCI469pGxfj/8= 144 | github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 h1:hgVxRoDDPtQE68PT4LFvNlPz2nBKd3OMlGKIQ69OmR4= 145 | github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531/go.mod h1:fqTUQpVYBvhCNIsMXGl2GE9q6z94DIP6NtFKXCSTVbg= 146 | github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d h1:J8tJzRyiddAFF65YVgxli+TyWBi0f79Sld6rJP6CBcY= 147 | github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d/go.mod h1:b+Q3v8Yrg5o15d71PSUraUzYb+jWl6wQMSBXSGS/hv0= 148 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 149 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 150 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 151 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 152 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 153 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 154 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 155 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 156 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 157 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 158 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 159 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 160 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 161 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 162 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 163 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 164 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 165 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 166 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 167 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 168 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 169 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 170 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 171 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 172 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 173 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 174 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 175 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= 176 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 177 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 178 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 179 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 180 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 181 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 182 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 183 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 184 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 185 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 186 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 187 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 188 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 189 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 190 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 191 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 192 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= 193 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= 194 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 195 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 196 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 197 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 198 | github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= 199 | github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 200 | github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= 201 | github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= 202 | github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= 203 | github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 204 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 205 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 206 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 207 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 208 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 209 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 210 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 211 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 212 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 213 | github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= 214 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 215 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 216 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 217 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 218 | github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= 219 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 220 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 221 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 222 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= 223 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 224 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 225 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 226 | github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= 227 | github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 228 | github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= 229 | github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= 230 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 231 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 232 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 233 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 234 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 235 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 236 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 237 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 238 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 239 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 240 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 241 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 242 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 243 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 244 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 245 | github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= 246 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 247 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 248 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= 249 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= 250 | go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= 251 | go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= 252 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= 253 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= 254 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8= 255 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8= 256 | go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= 257 | go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= 258 | go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= 259 | go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= 260 | go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= 261 | go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= 262 | go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= 263 | go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= 264 | go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= 265 | go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= 266 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 267 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 268 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 269 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 270 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 271 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 272 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 273 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 274 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 275 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 276 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 277 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 278 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 279 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 280 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 281 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 282 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 283 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 284 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 285 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 286 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 287 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 288 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 289 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 290 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 291 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 292 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 293 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 294 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 295 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 296 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 297 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 298 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 299 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 300 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 301 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 302 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 303 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 304 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 305 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 306 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 307 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 308 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 309 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 310 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 311 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 312 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 313 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 314 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 315 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 316 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 317 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 318 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 319 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 320 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 321 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 322 | google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= 323 | google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f h1:M65LEviCfuZTfrfzwwEoxVtgvfkFkBUbFnRbxCXuXhU= 324 | google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f/go.mod h1:Yo94eF2nj7igQt+TiJ49KxjIH8ndLYPZMIRSiRcEbg0= 325 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= 326 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= 327 | google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= 328 | google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= 329 | google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= 330 | google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 331 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 332 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 333 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 334 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 335 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 336 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 337 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 338 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 339 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 340 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 341 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 342 | gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= 343 | gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 344 | -------------------------------------------------------------------------------- /path_config.go: -------------------------------------------------------------------------------- 1 | package sshauth 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/vault/sdk/framework" 7 | "github.com/hashicorp/vault/sdk/helper/tokenutil" 8 | "github.com/hashicorp/vault/sdk/logical" 9 | ) 10 | 11 | func (b *backend) pathConfig() *framework.Path { 12 | p := &framework.Path{ 13 | Pattern: `config`, 14 | Fields: map[string]*framework.FieldSchema{ 15 | "ssh_ca_public_keys": { 16 | Type: framework.TypeCommaStringSlice, 17 | Description: `SSH CA public keys where ssh certificates are checked against.`, 18 | }, 19 | "secure_nonce": { 20 | Type: framework.TypeBool, 21 | Description: `Whether to use secure nonce generation.`, 22 | Default: true, 23 | }, 24 | }, 25 | 26 | Operations: map[logical.Operation]framework.OperationHandler{ 27 | logical.ReadOperation: &framework.PathOperation{ 28 | Callback: b.pathConfigRead, 29 | Summary: "Read the current SSH authentication backend configuration.", 30 | }, 31 | 32 | logical.UpdateOperation: &framework.PathOperation{ 33 | Callback: b.pathConfigWrite, 34 | Summary: "Configure the SSH authentication backend.", 35 | Description: confHelpDesc, 36 | }, 37 | }, 38 | 39 | HelpSynopsis: confHelpSyn, 40 | HelpDescription: confHelpDesc, 41 | } 42 | 43 | tokenutil.AddTokenFields(p.Fields) 44 | 45 | return p 46 | } 47 | 48 | func (b *backend) config(ctx context.Context, s logical.Storage) (*ConfigEntry, error) { 49 | entry, err := s.Get(ctx, "config") 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | if entry == nil { 55 | return nil, nil 56 | } 57 | 58 | config := &ConfigEntry{} 59 | 60 | if err := entry.DecodeJSON(config); err != nil { 61 | return nil, err 62 | } 63 | 64 | return config, nil 65 | } 66 | 67 | func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 68 | config, err := b.config(ctx, req.Storage) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | if config == nil { 74 | return nil, nil 75 | } 76 | 77 | d := map[string]interface{}{ 78 | "ssh_ca_public_keys": config.SSHCAPublicKeys, 79 | "secure_nonce": config.SecureNonce, 80 | } 81 | 82 | config.PopulateTokenData(d) 83 | 84 | return &logical.Response{ 85 | Data: d, 86 | }, nil 87 | } 88 | 89 | func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 90 | config := &ConfigEntry{ 91 | SSHCAPublicKeys: d.Get("ssh_ca_public_keys").([]string), 92 | SecureNonce: d.Get("secure_nonce").(bool), 93 | } 94 | 95 | if err := config.ParseTokenFields(req, d); err != nil { 96 | return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest 97 | } 98 | 99 | entry, err := logical.StorageEntryJSON("config", config) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | if err := req.Storage.Put(ctx, entry); err != nil { 105 | return nil, err 106 | } 107 | 108 | return nil, nil 109 | } 110 | 111 | const ( 112 | confHelpSyn = ` 113 | Configures the SSH authentication backend. 114 | ` 115 | confHelpDesc = ` 116 | The SSH authentication backend validates ssh certificates and public keys. 117 | ` 118 | ) 119 | -------------------------------------------------------------------------------- /path_login.go: -------------------------------------------------------------------------------- 1 | package sshauth 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | 7 | "github.com/hashicorp/vault/sdk/framework" 8 | "github.com/hashicorp/vault/sdk/helper/cidrutil" 9 | "github.com/hashicorp/vault/sdk/logical" 10 | ) 11 | 12 | func (b *backend) pathLogin() *framework.Path { 13 | return &framework.Path{ 14 | Pattern: "login$", 15 | Fields: map[string]*framework.FieldSchema{ 16 | "role": { 17 | Type: framework.TypeString, 18 | Description: "Role to use", 19 | }, 20 | "metadata": { 21 | Type: framework.TypeKVPairs, 22 | Description: "Keys and values to set for alias metadata", 23 | }, 24 | "cert": { 25 | Type: framework.TypeString, 26 | Description: "SSH certificate (base64 encoded)", 27 | }, 28 | "public_key": { 29 | Type: framework.TypeString, 30 | Description: "SSH public key (base64 encoded)", 31 | }, 32 | "signature": { 33 | Type: framework.TypeString, 34 | Description: "Signature over the nonce (base64 encoded)", 35 | }, 36 | "nonce": { 37 | Type: framework.TypeString, 38 | Description: "Nonce (base64 encoded)", 39 | }, 40 | "username": { 41 | Type: framework.TypeString, 42 | Description: "Principal to use when authenticating using ssh certificate (otherwise autodiscovered)", 43 | }, 44 | }, 45 | Operations: map[logical.Operation]framework.OperationHandler{ 46 | logical.UpdateOperation: &framework.PathOperation{ 47 | Callback: b.handleLogin, 48 | Summary: "Log in using ssh certificates", 49 | }, 50 | logical.AliasLookaheadOperation: &framework.PathOperation{ 51 | Callback: b.handleLogin, 52 | }, 53 | }, 54 | } 55 | } 56 | 57 | func (b *backend) handleLogin(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 58 | config, err := b.config(ctx, req.Storage) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | if config == nil { 64 | return logical.ErrorResponse("could not load configuration"), nil 65 | } 66 | 67 | roleName := data.Get("role").(string) 68 | if roleName == "" { 69 | return logical.ErrorResponse("role must be provided"), nil 70 | } 71 | 72 | role, err := b.role(ctx, req.Storage, roleName) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | if role == nil { 78 | return logical.ErrorResponse("role %q could not be found", roleName), nil 79 | } 80 | 81 | validPrincipals := []string{roleName} 82 | 83 | // if we have explicit principals we must check those 84 | if len(role.Principals) > 0 { 85 | validPrincipals = role.Principals 86 | } 87 | 88 | if len(role.TokenBoundCIDRs) > 0 { 89 | if req.Connection == nil { 90 | b.Logger().Warn("token bound CIDRs found but no connection information available for validation") 91 | return nil, logical.ErrPermissionDenied 92 | } 93 | 94 | if !cidrutil.RemoteAddrIsOk(req.Connection.RemoteAddr, role.TokenBoundCIDRs) { 95 | return nil, logical.ErrPermissionDenied 96 | } 97 | } 98 | 99 | signature := data.Get("signature").(string) 100 | if signature == "" { 101 | return logical.ErrorResponse("signature must be provided"), nil 102 | } 103 | 104 | cert := data.Get("cert").(string) 105 | 106 | pubkey := data.Get("public_key").(string) 107 | 108 | if pubkey == "" && cert == "" { 109 | return logical.ErrorResponse("cert or pubkey must be provided"), nil 110 | } 111 | 112 | nonce := data.Get("nonce").(string) 113 | if nonce == "" { 114 | return logical.ErrorResponse("nonce must be provided"), nil 115 | } 116 | 117 | sigDecode, err := base64.StdEncoding.DecodeString(signature) 118 | if err != nil { 119 | return logical.ErrorResponse("decoding signature failed"), nil 120 | } 121 | 122 | nonceDecode, err := base64.StdEncoding.DecodeString(nonce) 123 | if err != nil { 124 | return logical.ErrorResponse("decoding nonce failed"), nil 125 | } 126 | 127 | toParseKey := cert 128 | if toParseKey == "" { 129 | toParseKey = pubkey 130 | } 131 | 132 | if !b.nonceValidate(config, nonceDecode) { 133 | return logical.ErrorResponse("nonce time expired or invalid"), nil 134 | } 135 | 136 | pk, err := parsePubkey(toParseKey) 137 | if err != nil { 138 | return logical.ErrorResponse("decoding public_key/cert failed: " + err.Error()), err 139 | } 140 | 141 | if err := verifySignature(pk, nonceDecode, sigDecode); err != nil { 142 | return logical.ErrorResponse(err.Error()), err 143 | } 144 | 145 | // Name for the logical.Alias to set 146 | aliasName := roleName 147 | 148 | if cert != "" { 149 | principal := data.Get("username").(string) 150 | 151 | if principal == "" { 152 | // Select the first valid principal in the certificate, if any listed 153 | principal = detectPrincipal(pk, validPrincipals) 154 | } 155 | 156 | if err := validatePrincipal(principal, validPrincipals); err != nil { 157 | return logical.ErrorResponse("validation cert failed: " + err.Error()), err 158 | } 159 | 160 | if err := validateCert(pk, principal, config); err != nil { 161 | return logical.ErrorResponse("validation cert failed: " + err.Error()), err 162 | } 163 | 164 | // Take the principal as the alias name 165 | aliasName = principal 166 | } else { 167 | if err := validatePubkey(pubkey, role); err != nil { 168 | return logical.ErrorResponse("validation public_key failed: " + err.Error()), err 169 | } 170 | } 171 | 172 | metadata := map[string]string{} 173 | if metadataRaw, ok := data.GetOk("metadata"); ok { 174 | for key, value := range metadataRaw.(map[string]string) { 175 | metadata[key] = value 176 | } 177 | } 178 | // Set role last in case need to override something user set 179 | metadata["role"] = roleName 180 | 181 | // Compose the response 182 | resp := &logical.Response{} 183 | auth := &logical.Auth{ 184 | InternalData: map[string]interface{}{ 185 | "role": roleName, 186 | }, 187 | Metadata: metadata, 188 | DisplayName: aliasName, 189 | Alias: &logical.Alias{ 190 | Name: aliasName, 191 | Metadata: metadata, 192 | }, 193 | } 194 | 195 | role.PopulateTokenAuth(auth) 196 | 197 | resp.Auth = auth 198 | 199 | return resp, nil 200 | } 201 | -------------------------------------------------------------------------------- /path_nonce.go: -------------------------------------------------------------------------------- 1 | package sshauth 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/hashicorp/go-uuid" 8 | "github.com/hashicorp/vault/sdk/framework" 9 | "github.com/hashicorp/vault/sdk/logical" 10 | ) 11 | 12 | func (b *backend) pathNonce() *framework.Path { 13 | return &framework.Path{ 14 | Pattern: "nonce$", 15 | Operations: map[logical.Operation]framework.OperationHandler{ 16 | logical.ReadOperation: &framework.PathOperation{ 17 | Callback: b.pathNonceRead, 18 | Summary: "Generates a new nonce", 19 | }, 20 | }, 21 | } 22 | } 23 | 24 | func (b *backend) pathNonceRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 25 | nonce, err := uuid.GenerateUUID() 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | b.nonceLock.Lock() 31 | b.nonces[nonce] = time.Now() 32 | b.nonceLock.Unlock() 33 | 34 | return &logical.Response{ 35 | Data: map[string]interface{}{ 36 | "nonce": nonce, 37 | }, 38 | }, nil 39 | } 40 | 41 | func (b *backend) nonceValidate(config *ConfigEntry, nonce []byte) bool { 42 | b.nonceLock.RLock() 43 | defer b.nonceLock.RUnlock() 44 | 45 | if t, ok := b.nonces[string(nonce)]; ok { 46 | delete(b.nonces, string(nonce)) 47 | return time.Since(t) <= time.Second*30 48 | } 49 | 50 | if config.SecureNonce { 51 | return false 52 | } 53 | 54 | return validNonceTime([]byte(nonce)) 55 | } 56 | 57 | func (b *backend) nonceCleanup() { 58 | b.nonceLock.Lock() 59 | defer b.nonceLock.Unlock() 60 | 61 | for k, v := range b.nonces { 62 | if time.Since(v) > time.Second*30 { 63 | delete(b.nonces, k) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /path_role.go: -------------------------------------------------------------------------------- 1 | package sshauth 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/hashicorp/vault/sdk/framework" 9 | "github.com/hashicorp/vault/sdk/helper/tokenutil" 10 | "github.com/hashicorp/vault/sdk/logical" 11 | "golang.org/x/crypto/ssh" 12 | ) 13 | 14 | func (b *backend) pathRoleList() *framework.Path { 15 | return &framework.Path{ 16 | Pattern: "role/?", 17 | Operations: map[logical.Operation]framework.OperationHandler{ 18 | logical.ListOperation: &framework.PathOperation{ 19 | Callback: b.doPathRoleList, 20 | Summary: strings.TrimSpace(roleHelp["role-list"][0]), 21 | Description: strings.TrimSpace(roleHelp["role-list"][1]), 22 | }, 23 | }, 24 | HelpSynopsis: strings.TrimSpace(roleHelp["role-list"][0]), 25 | HelpDescription: strings.TrimSpace(roleHelp["role-list"][1]), 26 | } 27 | } 28 | 29 | // pathRole returns the path configurations for the CRUD operations on roles 30 | func (b *backend) pathRole() *framework.Path { 31 | p := &framework.Path{ 32 | Pattern: "role/" + framework.GenericNameWithAtRegex("name"), 33 | Fields: map[string]*framework.FieldSchema{ 34 | "name": { 35 | Type: framework.TypeLowerCaseString, 36 | Description: "Name of the role.", 37 | }, 38 | "public_keys": { 39 | Type: framework.TypeCommaStringSlice, 40 | Description: "Public keys allowed for this role.", 41 | }, 42 | "principals": { 43 | Type: framework.TypeCommaStringSlice, 44 | Description: "Principals allowed for this role. A * means every principal is accepted.", 45 | }, 46 | }, 47 | ExistenceCheck: b.pathRoleExistenceCheck, 48 | Operations: map[logical.Operation]framework.OperationHandler{ 49 | logical.ReadOperation: &framework.PathOperation{ 50 | Callback: b.pathRoleRead, 51 | Summary: "Read an existing role.", 52 | }, 53 | 54 | logical.UpdateOperation: &framework.PathOperation{ 55 | Callback: b.pathRoleCreateUpdate, 56 | Summary: strings.TrimSpace(roleHelp["role"][0]), 57 | Description: strings.TrimSpace(roleHelp["role"][1]), 58 | }, 59 | 60 | logical.CreateOperation: &framework.PathOperation{ 61 | Callback: b.pathRoleCreateUpdate, 62 | Summary: strings.TrimSpace(roleHelp["role"][0]), 63 | Description: strings.TrimSpace(roleHelp["role"][1]), 64 | }, 65 | 66 | logical.DeleteOperation: &framework.PathOperation{ 67 | Callback: b.pathRoleDelete, 68 | Summary: "Delete an existing role.", 69 | }, 70 | }, 71 | HelpSynopsis: strings.TrimSpace(roleHelp["role"][0]), 72 | HelpDescription: strings.TrimSpace(roleHelp["role"][1]), 73 | } 74 | 75 | tokenutil.AddTokenFields(p.Fields) 76 | 77 | return p 78 | } 79 | 80 | type sshRole struct { 81 | tokenutil.TokenParams 82 | 83 | PublicKeys []string `json:"public_keys"` 84 | Principals []string `json:"principals"` 85 | } 86 | 87 | // role takes a storage backend and the name and returns the role's storage 88 | // entry 89 | func (b *backend) role(ctx context.Context, s logical.Storage, name string) (*sshRole, error) { 90 | raw, err := s.Get(ctx, rolePrefix+name) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | if raw == nil { 96 | return nil, nil 97 | } 98 | 99 | role := new(sshRole) 100 | if err := raw.DecodeJSON(role); err != nil { 101 | return nil, err 102 | } 103 | 104 | return role, nil 105 | } 106 | 107 | // pathRoleExistenceCheck returns whether the role with the given name exists or not. 108 | func (b *backend) pathRoleExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) { 109 | role, err := b.role(ctx, req.Storage, data.Get("name").(string)) 110 | if err != nil { 111 | return false, err 112 | } 113 | 114 | return role != nil, nil 115 | } 116 | 117 | // pathRoleList is used to list all the Roles registered with the backend. 118 | func (b *backend) doPathRoleList(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 119 | roles, err := req.Storage.List(ctx, rolePrefix) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | return logical.ListResponse(roles), nil 125 | } 126 | 127 | // pathRoleRead grabs a read lock and reads the options set on the role from the storage 128 | func (b *backend) pathRoleRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 129 | roleName := data.Get("name").(string) 130 | if roleName == "" { 131 | return logical.ErrorResponse("missing name"), nil 132 | } 133 | 134 | role, err := b.role(ctx, req.Storage, roleName) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | if role == nil { 140 | return nil, nil 141 | } 142 | 143 | // Create a map of data to be returned 144 | d := map[string]interface{}{ 145 | "principals": role.Principals, 146 | "public_keys": role.PublicKeys, 147 | } 148 | 149 | role.PopulateTokenData(d) 150 | 151 | return &logical.Response{ 152 | Data: d, 153 | }, nil 154 | } 155 | 156 | // pathRoleDelete removes the role from storage 157 | func (b *backend) pathRoleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 158 | roleName := data.Get("name").(string) 159 | if roleName == "" { 160 | return logical.ErrorResponse("role name required"), nil 161 | } 162 | 163 | // Delete the role itself 164 | if err := req.Storage.Delete(ctx, rolePrefix+roleName); err != nil { 165 | return nil, err 166 | } 167 | 168 | return nil, nil 169 | } 170 | 171 | // pathRoleCreateUpdate registers a new role with the backend or updates the options 172 | // of an existing role 173 | func (b *backend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 174 | roleName := data.Get("name").(string) 175 | if roleName == "" { 176 | return logical.ErrorResponse("missing role name"), nil 177 | } 178 | 179 | // Check if the role already exists 180 | role, err := b.role(ctx, req.Storage, roleName) 181 | if err != nil { 182 | return nil, err 183 | } 184 | 185 | // Create a new entry object if this is a CreateOperation 186 | if role == nil { 187 | if req.Operation == logical.UpdateOperation { 188 | return logical.ErrorResponse("role entry not found during update operation"), nil 189 | } 190 | 191 | role = new(sshRole) 192 | 193 | // set defaults for token parameters 194 | config, err := b.config(ctx, req.Storage) 195 | if err != nil { 196 | return nil, err 197 | } 198 | 199 | if config != nil { 200 | role.TokenParams = config.TokenParams 201 | } 202 | } 203 | 204 | if publicKeys, ok := data.GetOk("public_keys"); ok { 205 | role.PublicKeys = publicKeys.([]string) 206 | } 207 | 208 | for idx, key := range role.PublicKeys { 209 | certParsed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key)) 210 | if err != nil { 211 | return logical.ErrorResponse("public_keys parsing failed: %s", err), nil 212 | } 213 | 214 | role.PublicKeys[idx] = strings.TrimRight(string(ssh.MarshalAuthorizedKey(certParsed)), "\n") 215 | } 216 | 217 | if principals, ok := data.GetOk("principals"); ok { 218 | role.Principals = principals.([]string) 219 | } 220 | 221 | if len(role.Principals) > 0 && len(role.PublicKeys) > 0 { 222 | return logical.ErrorResponse("public_keys and principals option are mutually exclusive"), nil 223 | } 224 | 225 | if err = role.ParseTokenFields(req, data); err != nil { 226 | return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest 227 | } 228 | 229 | if role.TokenPeriod > b.System().MaxLeaseTTL() { 230 | return logical.ErrorResponse(fmt.Sprintf("'period' of '%q' is greater than the backend's maximum lease TTL of '%q'", role.TokenPeriod.String(), b.System().MaxLeaseTTL().String())), nil 231 | } 232 | 233 | // Check that the TTL value provided is less than the MaxTTL. 234 | // Sanitizing the TTL and MaxTTL is not required now and can be performed 235 | // at credential issue time. 236 | if role.TokenMaxTTL > 0 && role.TokenTTL > role.TokenMaxTTL { 237 | return logical.ErrorResponse("ttl should not be greater than max ttl"), nil 238 | } 239 | 240 | resp := &logical.Response{} 241 | 242 | if role.TokenMaxTTL > b.System().MaxLeaseTTL() { 243 | resp.AddWarning("token max ttl is greater than the system or backend mount's maximum TTL value; issued tokens' max TTL value will be truncated") 244 | } 245 | 246 | // Store the entry. 247 | entry, err := logical.StorageEntryJSON(rolePrefix+roleName, role) 248 | if err != nil { 249 | return nil, err 250 | } 251 | 252 | if err = req.Storage.Put(ctx, entry); err != nil { 253 | return nil, err 254 | } 255 | 256 | return resp, nil 257 | } 258 | 259 | // roleStorageEntry stores all the options that are set on an role 260 | var roleHelp = map[string][2]string{ 261 | "role-list": { 262 | "Lists all the roles registered with the backend.", 263 | "The list will contain the names of the roles.", 264 | }, 265 | "role": { 266 | "Register an role with the backend.", 267 | `A role is required to authenticate with this backend. The role binds 268 | ssh information with token policies and settings. 269 | The bindings, token polices and token settings can all be configured 270 | using this endpoint`, 271 | }, 272 | } 273 | -------------------------------------------------------------------------------- /path_role_test.go: -------------------------------------------------------------------------------- 1 | package sshauth 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | log "github.com/hashicorp/go-hclog" 10 | "github.com/hashicorp/go-sockaddr" 11 | "github.com/hashicorp/vault/sdk/helper/logging" 12 | "github.com/hashicorp/vault/sdk/helper/tokenutil" 13 | "github.com/hashicorp/vault/sdk/logical" 14 | ) 15 | 16 | func getBackend(t *testing.T) (logical.Backend, logical.Storage) { 17 | defaultLeaseTTLVal := time.Hour * 12 18 | maxLeaseTTLVal := time.Hour * 24 19 | 20 | config := &logical.BackendConfig{ 21 | Logger: logging.NewVaultLogger(log.Trace), 22 | 23 | System: &logical.StaticSystemView{ 24 | DefaultLeaseTTLVal: defaultLeaseTTLVal, 25 | MaxLeaseTTLVal: maxLeaseTTLVal, 26 | }, 27 | StorageView: &logical.InmemStorage{}, 28 | } 29 | b, err := Factory(context.Background(), config) 30 | if err != nil { 31 | t.Fatalf("unable to create backend: %v", err) 32 | } 33 | 34 | return b, config.StorageView 35 | } 36 | 37 | func TestPath_Create(t *testing.T) { 38 | t.Run("happy path principals", func(t *testing.T) { 39 | b, storage := getBackend(t) 40 | 41 | data := map[string]interface{}{ 42 | "token_bound_cidrs": "127.0.0.1/8", 43 | "token_policies": "test", 44 | "token_period": "3s", 45 | "token_ttl": "1s", 46 | "token_num_uses": 12, 47 | "token_max_ttl": "5s", 48 | "principals": "ubuntu,ubuntu2", 49 | } 50 | 51 | expectedSockAddr, err := sockaddr.NewSockAddr("127.0.0.1/8") 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | expected := &sshRole{ 57 | TokenParams: tokenutil.TokenParams{ 58 | TokenPolicies: []string{"test"}, 59 | TokenPeriod: 3 * time.Second, 60 | TokenTTL: 1 * time.Second, 61 | TokenMaxTTL: 5 * time.Second, 62 | TokenNumUses: 12, 63 | TokenBoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: expectedSockAddr}}, 64 | }, 65 | Principals: []string{"ubuntu", "ubuntu2"}, 66 | PublicKeys: []string(nil), 67 | } 68 | 69 | req := &logical.Request{ 70 | Operation: logical.CreateOperation, 71 | Path: "role/plugin-test", 72 | Storage: storage, 73 | Data: data, 74 | } 75 | 76 | resp, err := b.HandleRequest(context.Background(), req) 77 | if err != nil || (resp != nil && resp.IsError()) { 78 | t.Fatalf("err:%s resp:%#v\n", err, resp) 79 | } 80 | actual, err := b.(*backend).role(context.Background(), storage, "plugin-test") 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | 85 | if !reflect.DeepEqual(expected, actual) { 86 | t.Fatalf("Unexpected role data: expected %#v\n got %#v\n", expected, actual) 87 | } 88 | }) 89 | 90 | t.Run("happy path public key", func(t *testing.T) { 91 | b, storage := getBackend(t) 92 | 93 | data := map[string]interface{}{ 94 | "token_bound_cidrs": "2001:0db8::/64", 95 | "token_policies": "test", 96 | "token_period": "3s", 97 | "token_ttl": "1s", 98 | "token_num_uses": 12, 99 | "token_max_ttl": "5s", 100 | "public_keys": []string{"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGg0xzFrvEYbZGkF5vWlHUutACUTLH7WMUG09NOi6skL"}, 101 | } 102 | 103 | expectedSockAddr, err := sockaddr.NewSockAddr("2001:0db8::/64") 104 | if err != nil { 105 | t.Fatal(err) 106 | } 107 | 108 | expected := &sshRole{ 109 | TokenParams: tokenutil.TokenParams{ 110 | TokenPolicies: []string{"test"}, 111 | TokenPeriod: 3 * time.Second, 112 | TokenTTL: 1 * time.Second, 113 | TokenMaxTTL: 5 * time.Second, 114 | TokenNumUses: 12, 115 | TokenBoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: expectedSockAddr}}, 116 | }, 117 | PublicKeys: []string{"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGg0xzFrvEYbZGkF5vWlHUutACUTLH7WMUG09NOi6skL"}, 118 | Principals: []string(nil), 119 | } 120 | 121 | req := &logical.Request{ 122 | Operation: logical.CreateOperation, 123 | Path: "role/plugin-test2", 124 | Storage: storage, 125 | Data: data, 126 | } 127 | 128 | resp, err := b.HandleRequest(context.Background(), req) 129 | if err != nil || (resp != nil && resp.IsError()) { 130 | t.Fatalf("err:%s resp:%#v\n", err, resp) 131 | } 132 | actual, err := b.(*backend).role(context.Background(), storage, "plugin-test2") 133 | if err != nil { 134 | t.Fatal(err) 135 | } 136 | 137 | if !reflect.DeepEqual(expected, actual) { 138 | t.Fatalf("Unexpected role data: expected %#v\n got %#v\n", expected, actual) 139 | } 140 | }) 141 | 142 | t.Run("publickey and cert", func(t *testing.T) { 143 | b, storage := getBackend(t) 144 | data := map[string]interface{}{ 145 | "policies": "test", 146 | "public_keys": []string{"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGg0xzFrvEYbZGkF5vWlHUutACUTLH7WMUG09NOi6skL"}, 147 | "principals": []string{"ubuntu", "ubuntu2"}, 148 | } 149 | 150 | req := &logical.Request{ 151 | Operation: logical.CreateOperation, 152 | Path: "role/test2", 153 | Storage: storage, 154 | Data: data, 155 | } 156 | 157 | resp, err := b.HandleRequest(context.Background(), req) 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | if resp != nil && !resp.IsError() { 162 | t.Fatalf("expected error") 163 | } 164 | if resp.Error().Error() != "public_keys and principals option are mutually exclusive" { 165 | t.Fatalf("unexpected err: %v", resp) 166 | } 167 | }) 168 | 169 | t.Run("invalid public key", func(t *testing.T) { 170 | b, storage := getBackend(t) 171 | data := map[string]interface{}{ 172 | "policies": "test", 173 | "public_keys": []string{"ssh-ed25519 AAXAAC3NzaC1lZDI1NTE5AAAAIGg0xzFrvEYbZGkF5vWlHUutACUTLH7WMUG09NOi6skL"}, 174 | } 175 | 176 | req := &logical.Request{ 177 | Operation: logical.CreateOperation, 178 | Path: "role/test3", 179 | Storage: storage, 180 | Data: data, 181 | } 182 | 183 | resp, err := b.HandleRequest(context.Background(), req) 184 | if err != nil { 185 | t.Fatal(err) 186 | } 187 | if resp != nil && !resp.IsError() { 188 | t.Fatalf("expected error") 189 | } 190 | if resp.Error().Error() != "public_keys parsing failed: ssh: no key found" { 191 | t.Fatalf("unexpected err: %v", resp) 192 | } 193 | }) 194 | 195 | t.Run("invalid public key one", func(t *testing.T) { 196 | b, storage := getBackend(t) 197 | data := map[string]interface{}{ 198 | "policies": "test", 199 | "public_keys": []string{"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGg0xzFrvEYbZGkF5vWlHUutACUTLH7WMUG09NOi6skL", 200 | "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGg0xzFrvEYbZGkF5vWlHUutACUTLH7WMUG09NOi6skLX"}, 201 | } 202 | 203 | req := &logical.Request{ 204 | Operation: logical.CreateOperation, 205 | Path: "role/test4", 206 | Storage: storage, 207 | Data: data, 208 | } 209 | 210 | resp, err := b.HandleRequest(context.Background(), req) 211 | if err != nil { 212 | t.Fatal(err) 213 | } 214 | if resp != nil && !resp.IsError() { 215 | t.Fatalf("expected error") 216 | } 217 | if resp.Error().Error() != "public_keys parsing failed: ssh: no key found" { 218 | t.Fatalf("unexpected err: %v", resp) 219 | } 220 | }) 221 | } 222 | -------------------------------------------------------------------------------- /pylogin/README.md: -------------------------------------------------------------------------------- 1 | # pylogin 2 | 3 | This is a python code example which uses vault-plugin-auth-ssh set up 4 | with ssh public keys in ssh-agent to get a vault token. 5 | 6 | Requirements: 7 | 8 | - ssh-agent which contains your ssh public key or keys. 9 | - $VAULT_ADDR which contains the URL to your vault server. 10 | - python3, including the paramiko module (installable for example 11 | through pip). 12 | 13 | If there are multiple keys in ssh-agent it will try each until success. 14 | If successful, stdout will have the json response including the vault 15 | token. You can extract it by piping it to `jq -r .auth.client_token`. 16 | 17 | The "-V" option will show the parameters it is sending to vault, and 18 | metadata keys and values may optionally be passed. 19 | 20 | ## Example 21 | 22 | Create a role `yourrole` which with known public_key and gives you the 23 | `apolicy` policy on this token. This has to be done using a privileged 24 | vault token. 25 | 26 | ``` 27 | $ vault write auth/ssh/role/yourrole token_policies="apolicy" public_keys=@id_rsa.pub 28 | ``` 29 | 30 | Now run login.py giving the role name and piping to jq: 31 | ``` 32 | $ ./login.py yourrole | jq -r .auth.client_token 33 | s.j0Sf3qCXxMRDqXXxWpxBuwgm 34 | ``` 35 | -------------------------------------------------------------------------------- /pylogin/login.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Example python program to login to vault-plugin-auth-ssh using ssh-agent 3 | # $VAULT_ADDR must be set, and role passed as an argument 4 | 5 | import sys 6 | import os 7 | import base64 8 | import struct 9 | import json 10 | import urllib.request 11 | import paramiko 12 | 13 | def usage(): 14 | print("Usage: " + me + " [-V] role [metadatakey=value ...]", file=sys.stderr) 15 | print(" -V: verbose, show data sent to vault", file=sys.stderr) 16 | sys.exit(1) 17 | 18 | def main(): 19 | argv = sys.argv 20 | global me 21 | me = sys.argv[0] 22 | verbose = False 23 | if len(argv) > 1: 24 | if argv[1] == "-V": 25 | verbose = True 26 | argv = argv[1:] 27 | elif argv[1][0] == '-': 28 | print(me + ': unknown option: ' + argv[1], file=sys.stderr) 29 | usage() 30 | if len(argv) < 2: 31 | usage() 32 | role = argv[1] 33 | 34 | vault = os.getenv("VAULT_ADDR") 35 | if vault is None: 36 | print("$VAULT_ADDR not set", file=sys.stderr) 37 | sys.exit(2) 38 | 39 | agent = paramiko.Agent() 40 | agent_keys = agent.get_keys() 41 | if len(agent_keys) == 0: 42 | print("No ssh agent keys found", file=sys.stderr) 43 | sys.exit(2) 44 | 45 | metadata = {} 46 | for arg in argv[2:]: 47 | parts = arg.split('=') 48 | if len(parts) > 1: 49 | metadata[parts[0]] = parts[1] 50 | 51 | errmsgs = [] 52 | for key in agent_keys: 53 | with urllib.request.urlopen(vault + "/v1/auth/ssh/nonce") as response: 54 | body = response.read() 55 | data = json.loads(body) 56 | nonce = data["data"]["nonce"] 57 | b64nonce = base64.b64encode(nonce.encode()).decode() 58 | 59 | d = key.sign_ssh_data(nonce) 60 | parts = [] 61 | while d: 62 | ln = struct.unpack('>I', d[:4])[0] 63 | bits = d[4:ln+4] 64 | parts.append(bits) 65 | d = d[ln+4:] 66 | sig = parts[1] 67 | b64sig = base64.b64encode(sig).decode() 68 | 69 | pubkey = key.get_name() + ' ' + key.get_base64() + ' x' 70 | data = { 71 | 'role': role, 72 | 'public_key': pubkey, 73 | 'signature': b64sig, 74 | 'nonce': b64nonce, 75 | } 76 | if metadata != {}: 77 | data['metadata'] = metadata 78 | 79 | datastr = json.dumps(data, indent=4, sort_keys=True) 80 | if verbose: 81 | print("-- Attempting login with\n" + datastr + "\n--", file=sys.stderr) 82 | 83 | req = urllib.request.Request(vault + "/v1/auth/ssh/login", datastr.encode()) 84 | try: 85 | with urllib.request.urlopen(req) as response: 86 | body = response.read() 87 | except Exception as e: 88 | typ = type(e).__name__ 89 | msg = typ + ': ' + str(e) 90 | if typ == 'HTTPError': 91 | errmsg = e.read().decode() 92 | try: 93 | decoded = json.loads(errmsg) 94 | if 'errors' in decoded: 95 | errmsg = ':' 96 | for error in decoded['errors']: 97 | errmsg += ' ' + error 98 | except: 99 | errmsg = ' ' + errmsg 100 | msg += errmsg 101 | if verbose: 102 | print('Login attempt failed: ' + msg, file=sys.stderr) 103 | else: 104 | errmsgs.append(msg) 105 | continue 106 | 107 | data = json.loads(body) 108 | print(json.dumps(data, indent=4, sort_keys=True)) 109 | 110 | sys.exit(0) 111 | 112 | for msg in errmsgs: 113 | print('Login failed: ' + msg, file=sys.stderr) 114 | sys.exit(1) 115 | 116 | if __name__ == '__main__': 117 | main() 118 | 119 | -------------------------------------------------------------------------------- /ssh.go: -------------------------------------------------------------------------------- 1 | package sshauth 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "github.com/hashicorp/vault/sdk/logical" 11 | "golang.org/x/crypto/ssh" 12 | ) 13 | 14 | func validatePubkey(pubkey string, role *sshRole) error { 15 | found := false 16 | 17 | pkParsed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubkey)) 18 | if err != nil { 19 | return fmt.Errorf("key parsing failed: %s", err) 20 | } 21 | 22 | pk := strings.TrimRight(string(ssh.MarshalAuthorizedKey(pkParsed)), "\n") 23 | 24 | for _, key := range role.PublicKeys { 25 | if key != pk { 26 | continue 27 | } 28 | 29 | found = true 30 | 31 | break 32 | } 33 | 34 | if !found { 35 | return logical.ErrPermissionDenied 36 | } 37 | 38 | return nil 39 | } 40 | 41 | // Detect the first valid principal from the certificate, if any is present 42 | func detectPrincipal(pubkey ssh.PublicKey, validPrincipals []string) string { 43 | cert, ok := pubkey.(*ssh.Certificate) 44 | if !ok { 45 | // Not a certificate - no principal to detect 46 | return "" 47 | } 48 | 49 | if len(cert.ValidPrincipals) == 0 { 50 | // Certificate is not valid for any principal 51 | return "" 52 | } 53 | 54 | // Return the first valid principal found in the certificate 55 | for _, p := range validPrincipals { 56 | for _, vp := range cert.ValidPrincipals { 57 | if p == vp || vp == "*" { 58 | return p 59 | } 60 | } 61 | } 62 | 63 | // No valid principal was found in the certificate 64 | return "" 65 | } 66 | 67 | func validatePrincipal(principal string, validPrincipals []string) error { 68 | if principal == "*" || principal == "" { 69 | return errors.New("invalid principal") 70 | } 71 | 72 | for _, vp := range validPrincipals { 73 | if vp == principal || vp == "*" { 74 | return nil 75 | } 76 | } 77 | 78 | return errors.New("no matching principal found") 79 | } 80 | 81 | func validateCert(pubkey ssh.PublicKey, principal string, config *ConfigEntry) error { 82 | cert, ok := pubkey.(*ssh.Certificate) 83 | if !ok { 84 | return errors.New("not a certificate") 85 | } 86 | 87 | if len(cert.ValidPrincipals) == 0 { 88 | return errors.New("refusing certificate without principal list") 89 | } 90 | 91 | c := &ssh.CertChecker{ 92 | IsUserAuthority: func(auth ssh.PublicKey) bool { 93 | for _, caKey := range config.SSHCAPublicKeys { 94 | pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(caKey)) 95 | if err != nil { 96 | return false 97 | } 98 | 99 | if bytes.Equal(auth.Marshal(), pubKey.Marshal()) { 100 | return true 101 | } 102 | } 103 | 104 | return false 105 | }, 106 | } 107 | 108 | // check the CA of the cert 109 | if !c.IsUserAuthority(cert.SignatureKey) { 110 | return errors.New("CA doesn't match") 111 | } 112 | 113 | // check cert validity 114 | if err := c.CheckCert(principal, cert); err != nil { 115 | return fmt.Errorf("certificate validation failed: %s", err) 116 | } 117 | 118 | return nil 119 | } 120 | 121 | func parsePubkey(pubkey string) (ssh.PublicKey, error) { 122 | certParsed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubkey)) 123 | if err != nil { 124 | return nil, fmt.Errorf("parsing failed %s", err) 125 | } 126 | 127 | parsedPubkey, err := ssh.ParsePublicKey(certParsed.Marshal()) 128 | if err != nil { 129 | return nil, fmt.Errorf("pubkey parsing failed %s", err) 130 | } 131 | 132 | return parsedPubkey, nil 133 | } 134 | 135 | func verifySignature(pubkey ssh.PublicKey, nonce, signature []byte) error { 136 | if cert, ok := pubkey.(*ssh.Certificate); ok { 137 | pubkey = cert.Key 138 | } 139 | 140 | sig := &ssh.Signature{ 141 | Format: pubkey.Type(), 142 | Blob: signature, 143 | } 144 | 145 | return pubkey.Verify(nonce, sig) 146 | } 147 | 148 | func validNonceTime(nonce []byte) bool { 149 | t := time.Time{} 150 | t.UnmarshalBinary(nonce) 151 | 152 | return time.Since(t) <= time.Second*30 153 | } 154 | -------------------------------------------------------------------------------- /vssh/README.md: -------------------------------------------------------------------------------- 1 | # vssh 2 | 3 | This is a quick code example which uses the vault-plugin-auth-ssh setup with ssh certificates to get a token. 4 | (You'll need something like this because we can't integrate directly with vault because this needs modifications of the vault source). 5 | 6 | Requirements: 7 | 8 | - ssh-agent which contains your ssh certificate. 9 | - VSSH_ROLE environment variable which contains the role you are going to use in auth/ssh/role/yourrole 10 | - VSSH_PRINCIPAL environment variable which contains the principal that is need to authenticate against the role in VSSH_ROLE 11 | - vssh will check every key in your ssh-agent to see if it matches a valid certificate containing this principal 12 | - the normal vault settings like VAULT_ADDR which contains the URL to your vault server. 13 | 14 | Then just run ./vssh and it'll output you a vault token that'll contain the policy you set on the role in auth/ssh/role/yourrole 15 | 16 | ## Example 17 | 18 | First add your ssh CA key (see ) 19 | 20 | ``` 21 | $ vault write auth/ssh/config ssh_ca_public_keys=@sshca 22 | ``` 23 | 24 | Create a role `yourrole` which needs a principal `ubuntu` in it's certificate and gives you the `apolicy` on this token. 25 | 26 | ``` 27 | $ vault write auth/ssh/role/yourrole token_policies="apolicy" principals="ubuntu" 28 | ``` 29 | 30 | Now run vssh 31 | 32 | ``` 33 | $ VSSH_ROLE=yourrole" VSSH_PRINCIPAL="ubuntu" vssh 34 | s.r4dGTu4tMvacKTEAXlKlRGtK 35 | ``` 36 | -------------------------------------------------------------------------------- /vssh/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rand" 6 | "encoding/base64" 7 | "fmt" 8 | "log" 9 | "net" 10 | "os" 11 | "strings" 12 | "time" 13 | 14 | "github.com/hashicorp/vault/api" 15 | "golang.org/x/crypto/ssh" 16 | "golang.org/x/crypto/ssh/agent" 17 | ) 18 | 19 | func main() { 20 | // talk to the agent 21 | ag, err := getAgent() 22 | if err != nil { 23 | log.Fatalf("ssh agent failed: %s", err) 24 | } 25 | 26 | // get all our agent keys 27 | sshsigners, err := ag.Signers() 28 | if err != nil { 29 | log.Fatalf("getting signers failed: %s", err) 30 | } 31 | 32 | principal := os.Getenv("VSSH_PRINCIPAL") 33 | role := os.Getenv("VSSH_ROLE") 34 | 35 | if principal == "" || role == "" { 36 | log.Fatalf("VSSH_ROLE or VSSH_PRINCIPAL is empty") 37 | } 38 | 39 | // find the specific certificate containing the principal we need 40 | signer := findCertSigner(sshsigners, principal) 41 | if signer == nil { 42 | log.Fatalf("no ssh key found: %s", err) 43 | } 44 | 45 | nonce, err := getNonce() 46 | if err != nil { 47 | log.Fatalf("nonce failed: %s", err) 48 | } 49 | 50 | token, err := getToken(nonce, role, signer) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | fmt.Println(token) 56 | } 57 | 58 | func getNonce() (string, error) { 59 | v, err := api.NewClient(api.DefaultConfig()) 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | // now login 65 | secret, err := v.Logical().Read("auth/ssh/nonce") 66 | if err != nil { 67 | return "", err 68 | } 69 | 70 | if secret == nil { 71 | return "", fmt.Errorf("empty response from credential provider") 72 | } 73 | 74 | return secret.Data["nonce"].(string), nil 75 | } 76 | 77 | func getToken(nonce, role string, signer ssh.Signer) (string, error) { 78 | v, err := api.NewClient(api.DefaultConfig()) 79 | if err != nil { 80 | return "", err 81 | } 82 | 83 | signBytes := []byte(nonce) 84 | 85 | // now sign this with our private key of the certificate 86 | res, _ := signer.Sign(rand.Reader, signBytes) 87 | 88 | signatureBlob := res.Blob 89 | 90 | // fill in all our required settings 91 | options := map[string]interface{}{ 92 | "role": role, 93 | "nonce": base64.StdEncoding.EncodeToString(signBytes), 94 | "signature": base64.StdEncoding.EncodeToString(signatureBlob), 95 | "cert": string(ssh.MarshalAuthorizedKey(signer.PublicKey())), 96 | } 97 | 98 | // now login 99 | secret, err := v.Logical().Write("auth/ssh/login", options) 100 | if err != nil { 101 | return "", err 102 | } 103 | 104 | if secret == nil { 105 | return "", fmt.Errorf("empty response from credential provider") 106 | } 107 | 108 | return secret.Auth.ClientToken, nil 109 | } 110 | 111 | // getAgent returns a ssh agent 112 | func getAgent() (agent.Agent, error) { 113 | sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | return agent.NewClient(sshAgent), nil 119 | } 120 | 121 | // findCertSigner returns the signer containing a valid certificate containing the specified principal 122 | func findCertSigner(sshsigners []ssh.Signer, principal string) ssh.Signer { 123 | for _, s := range sshsigners { 124 | // ignore non-certificate keys 125 | if !strings.Contains(s.PublicKey().Type(), "cert-v01@openssh.com") { 126 | continue 127 | } 128 | 129 | mpubkey, _ := ssh.ParsePublicKey(s.PublicKey().Marshal()) 130 | cryptopub := mpubkey.(crypto.PublicKey) 131 | cert := cryptopub.(*ssh.Certificate) 132 | t := time.Unix(int64(cert.ValidBefore), 0) 133 | 134 | if time.Until(t) <= time.Second*10 { 135 | continue 136 | } 137 | 138 | for _, p := range cert.ValidPrincipals { 139 | if principal == p { 140 | return s 141 | } 142 | } 143 | } 144 | 145 | return nil 146 | } 147 | --------------------------------------------------------------------------------