1
0
mirror of https://github.com/getsops/sops.git synced 2026-02-05 12:45:21 +01:00

hcvault: update API, add tests, tidy

This updates the Vault API and client to latest, adds more extensive
test coverage, and general tidying of bits of code.
The improvements are based on a fork of the key source in the Flux
project's kustomize-controller, built due to SOPS' limitation around
credential management without relying on runtime environment variables.

- Vault API and client have been updated to latest.
- It introduces a `Token` type which holds a Vault token, and can be
  applied to the `MasterKey`. When applied, the token is used in the
  Vault client configuration, instead of relying on the `VAULT_TOKEN`
  environment variables, or the `.vault-token` file in the user's home
  directory. This is most useful when working with SOPS as an SDK, in
  combination with e.g. a local key service server implementation.
- Extensive test coverage.

The forked version of this has compatability tests to ensure it works
with current SOPS:

- 62fb2d96a2/internal/sops/hcvault/keysource_test.go (L130)
- 62fb2d96a2/internal/sops/hcvault/keysource_test.go (L202)

Signed-off-by: Hidde Beydals <hello@hidde.co>
This commit is contained in:
Hidde Beydals
2022-07-04 23:46:43 +02:00
parent 8c5c397c46
commit c7ae3eee59
4 changed files with 718 additions and 320 deletions

8
go.mod
View File

@@ -22,7 +22,7 @@ require (
github.com/google/go-cmp v0.5.7
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/goware/prefixer v0.0.0-20160118172347-395022866408
github.com/hashicorp/vault/api v1.5.0
github.com/hashicorp/vault/api v1.7.2
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef
github.com/lib/pq v1.10.5
github.com/mitchellh/go-homedir v1.1.0
@@ -90,14 +90,14 @@ require (
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.3 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
github.com/hashicorp/go-uuid v1.0.2 // indirect
github.com/hashicorp/go-version v1.4.0 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/vault/sdk v0.4.1 // indirect
github.com/hashicorp/vault/sdk v0.5.1 // indirect
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
@@ -105,7 +105,7 @@ require (
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect

48
go.sum
View File

@@ -88,7 +88,6 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-metrics v0.3.10 h1:FR+drcQStOe+32sYyJYyZ7FIdgoGGBnwLl+flodp8Uo=
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@@ -140,7 +139,6 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -174,7 +172,6 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -196,30 +193,23 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk=
github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -333,42 +323,33 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM=
github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4=
github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw=
github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I=
github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 h1:p4AKXPPS24tO8Wc8i1gLvSKdmkiSY5xuju57czJ/IJQ=
github.com/hashicorp/go-secure-stdlib/mlock v0.1.2/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.3 h1:geBw3SBrxQq+buvbf4K+Qltv1gjaXJxy8asD4CjGYow=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.3/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs=
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4=
github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -377,10 +358,10 @@ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+l
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/vault/api v1.5.0 h1:Bp6yc2bn7CWkOrVIzFT/Qurzx528bdavF3nz590eu28=
github.com/hashicorp/vault/api v1.5.0/go.mod h1:LkMdrZnWNrFaQyYYazWVn7KshilfDidgVBq6YiTq/bM=
github.com/hashicorp/vault/sdk v0.4.1 h1:3SaHOJY687jY1fnB61PtL0cOkKItphrbLmux7T92HBo=
github.com/hashicorp/vault/sdk v0.4.1/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0=
github.com/hashicorp/vault/api v1.7.2 h1:kawHE7s/4xwrdKbkmwQi0wYaIeUhk5ueek7ljuezCVQ=
github.com/hashicorp/vault/api v1.7.2/go.mod h1:xbfA+1AvxFseDzxxdWaL0uO99n1+tndus4GCrtouy0M=
github.com/hashicorp/vault/sdk v0.5.1 h1:zly/TmNgOXCGgWIRA8GojyXzG817POtVh3uzIwzZx+8=
github.com/hashicorp/vault/sdk v0.5.1/go.mod h1:DoGraE9kKGNcVgPmTuX357Fm6WAx1Okvde8Vp3dPDoU=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 h1:xixZ2bWeofWV68J+x6AzmKuVM/JWCQwkWm6GW/MUR6I=
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
@@ -389,7 +370,6 @@ github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJ
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
@@ -407,13 +387,11 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ=
@@ -421,7 +399,6 @@ github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
@@ -433,13 +410,11 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
@@ -447,10 +422,8 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
@@ -478,7 +451,6 @@ github.com/ory/dockertest v3.3.5+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnh
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI=
@@ -584,7 +556,6 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88 h1:Tgea0cVUD0ivh5ADBX4WwuI12DUd2to3nCYe2eayMIw=
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -787,7 +758,6 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs=
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -990,7 +960,6 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
@@ -1019,7 +988,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=

View File

@@ -3,6 +3,7 @@ package hcvault
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"net/url"
@@ -14,34 +15,66 @@ import (
"time"
"github.com/hashicorp/vault/api"
homedir "github.com/mitchellh/go-homedir"
"github.com/mitchellh/go-homedir"
"github.com/sirupsen/logrus"
"go.mozilla.org/sops/v3/logging"
)
var log *logrus.Logger
func init() {
log = logging.NewLogger("VAULT_TRANSIT")
}
// MasterKey is a Vault Transit backend path used to encrypt and decrypt sops' data key.
type MasterKey struct {
EncryptedKey string
KeyName string
EnginePath string
VaultAddress string
CreationDate time.Time
var (
// log is the global logger for any Vault Transit MasterKey.
log *logrus.Logger
// vaultTTL is the duration after which a MasterKey requires rotation.
vaultTTL = time.Hour * 24 * 30 * 6
// defaultTokenFile is the name of the file in the user's home directory
// where a Vault token is expected to be stored.
defaultTokenFile = ".vault-token"
)
// Token used for authenticating towards a Vault server.
type Token string
// ApplyToMasterKey configures the token on the provided key.
func (t Token) ApplyToMasterKey(key *MasterKey) {
key.token = string(t)
}
// NewMasterKeysFromURIs gets lots of keys from lots of URIs
// MasterKey is a Vault Transit backend path used to Encrypt and Decrypt
// SOPS' data key.
type MasterKey struct {
// VaultAddress is the address of the Vault server.
VaultAddress string
// EnginePath is the path to the Vault Transit Secret engine relative
// to the VaultAddress.
EnginePath string
// KeyName is the name of the key in the Vault Transit engine.
KeyName string
// EncryptedKey contains the SOPS data key encrypted with the Vault Transit
// key.
EncryptedKey string
// CreationDate of the MasterKey, used to determine if the EncryptedKey
// needs rotation.
CreationDate time.Time
// token is the token used for authenticating against the VaultAddress
// server. It can be injected by a (local) keyservice.KeyServiceServer
// Token.ApplyToMasterKey. If empty, the default client configuration
// is used, before falling back to the token stored in defaultTokenFile.
token string
}
// NewMasterKeysFromURIs creates a list of MasterKeys from a list of Vault
// URIs.
func NewMasterKeysFromURIs(uris string) ([]*MasterKey, error) {
var keys []*MasterKey
if uris == "" {
return keys, nil
}
uriList := strings.Split(uris, ",")
for _, uri := range uriList {
for _, uri := range strings.Split(uris, ",") {
if uri == "" {
continue
}
@@ -54,9 +87,9 @@ func NewMasterKeysFromURIs(uris string) ([]*MasterKey, error) {
return keys, nil
}
// NewMasterKeyFromURI obtains the vaultAddress the transit backend path and the key name from the full URI of the key
// NewMasterKeyFromURI obtains the Vault address, Transit backend path and the
// key name from the full URI of the key.
func NewMasterKeyFromURI(uri string) (*MasterKey, error) {
log.Debugln("Called NewMasterKeyFromURI with uri: ", uri)
var key *MasterKey
if uri == "" {
return key, nil
@@ -66,9 +99,10 @@ func NewMasterKeyFromURI(uri string) (*MasterKey, error) {
return nil, err
}
if u.Scheme == "" {
return nil, fmt.Errorf("missing scheme in vault URL (should be like this: https://vault.example.com:8200/v1/transit/keys/keyName), got: %v", uri)
return nil, fmt.Errorf("missing scheme in Vault URL (should be like this: +"+
"https://vault.example.com:8200/v1/transit/keys/keyName), got: %v", uri)
}
enginePath, keyName, err := getBackendAndKeyFromPath(u.RequestURI())
enginePath, keyName, err := engineAndKeyFromPath(u.RequestURI())
if err != nil {
return nil, err
}
@@ -77,110 +111,47 @@ func NewMasterKeyFromURI(uri string) (*MasterKey, error) {
}
func getBackendAndKeyFromPath(fullPath string) (enginePath, keyName string, err error) {
// Running vault behind a reverse proxy with longer urls seems not to be supported
// by the vault client api so we have a separate Error for that here.
if re := regexp.MustCompile(`/[^/]+/v[\d]+/[^/]+/[^/]+/[^/]+`); re.Match([]byte(fullPath)) {
return "", "", fmt.Errorf("running Vault with a prefixed url is not supported! (Format has to be like https://vault.example.com:8200/v1/transit/keys/keyName)")
} else if re := regexp.MustCompile(`/v[\d]+/[^/]+/[^/]+/[^/]+`); re.Match([]byte(fullPath)) == false {
return "", "", fmt.Errorf("vault path does not seem to be formatted correctly: (eg. https://vault.example.com:8200/v1/transit/keys/keyName)")
}
fullPath = strings.TrimPrefix(fullPath, "/")
fullPath = strings.TrimSuffix(fullPath, "/")
dirs := strings.Split(fullPath, "/")
keyName = dirs[len(dirs)-1]
enginePath = path.Join(dirs[1 : len(dirs)-2]...)
err = nil
return
}
// NewMasterKey creates a new MasterKey from a vault address, transit backend path and a key name and setting the creation date to the current date
func NewMasterKey(addess, enginePath, keyName string) *MasterKey {
mk := &MasterKey{
VaultAddress: addess,
// NewMasterKey creates a new MasterKey from a Vault address, Transit backend
// path and a key name.
func NewMasterKey(address, enginePath, keyName string) *MasterKey {
key := &MasterKey{
VaultAddress: address,
EnginePath: enginePath,
KeyName: keyName,
CreationDate: time.Now().UTC(),
}
log.Debugln("Created Vault Master Key: ", mk)
return mk
return key
}
// EncryptedDataKey returns the encrypted data key this master key holds
func (key *MasterKey) EncryptedDataKey() []byte {
return []byte(key.EncryptedKey)
}
// SetEncryptedDataKey sets the encrypted data key for this master key
func (key *MasterKey) SetEncryptedDataKey(enc []byte) {
key.EncryptedKey = string(enc)
}
func vaultClient(address string) (*api.Client, error) {
cfg := api.DefaultConfig()
cfg.Address = address
cli, err := api.NewClient(cfg)
if err != nil {
return nil, fmt.Errorf("Cannot create Vault Client: %w", err)
}
if cli.Token() != "" {
return cli, nil
}
homePath, err := homedir.Dir()
if err != nil {
panic(fmt.Sprintf("error getting user's home directory: %v", err))
}
tokenPath := filepath.Join(homePath, ".vault-token")
f, err := os.Open(tokenPath)
if os.IsNotExist(err) {
return cli, nil
}
if err != nil {
return nil, err
}
defer f.Close()
buf := bytes.NewBuffer(nil)
if _, err := io.Copy(buf, f); err != nil {
return nil, err
}
cli.SetToken(strings.TrimSpace(buf.String()))
return cli, nil
}
// Encrypt takes a sops data key, encrypts it with Vault Transit and stores the result in the EncryptedKey field
// Encrypt takes a SOPS data key, encrypts it with Vault Transit, and stores
// the result in the EncryptedKey field.
func (key *MasterKey) Encrypt(dataKey []byte) error {
fullPath := path.Join(key.EnginePath, "encrypt", key.KeyName)
cli, err := vaultClient(key.VaultAddress)
fullPath := key.encryptPath()
client, err := vaultClient(key.VaultAddress, key.token)
if err != nil {
log.WithError(err).WithField("Path", fullPath).Error("Encryption failed")
return err
}
encoded := base64.StdEncoding.EncodeToString(dataKey)
payload := make(map[string]interface{})
payload["plaintext"] = encoded
raw, err := cli.Logical().Write(fullPath, payload)
secret, err := client.Logical().Write(fullPath, encryptPayload(dataKey))
if err != nil {
log.WithField("Path", fullPath).Info("Encryption failed")
return err
log.WithError(err).WithField("Path", fullPath).Error("Encryption failed")
return fmt.Errorf("failed to encrypt sops data key to Vault transit backend '%s': %w", fullPath, err)
}
if raw == nil || raw.Data == nil {
return fmt.Errorf("The transit backend %s is empty", fullPath)
}
encrypted, ok := raw.Data["ciphertext"]
if !ok {
return fmt.Errorf("there's not encrypted data")
}
encryptedKey, ok := encrypted.(string)
if !ok {
return fmt.Errorf("the ciphertext cannot be casted to string")
encryptedKey, err := encryptedKeyFromSecret(secret)
if err != nil {
log.WithError(err).WithField("Path", fullPath).Error("Encryption failed")
return fmt.Errorf("failed to encrypt sops data key to Vault transit backend '%s': %w", fullPath, err)
}
key.EncryptedKey = encryptedKey
log.WithField("Path", fullPath).Info("Encryption successful")
return nil
}
// EncryptIfNeeded encrypts the provided sops' data key and encrypts it if it hasn't been encrypted yet
// EncryptIfNeeded encrypts the provided SOPS data key, if it has not been
// encrypted yet.
func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error {
if key.EncryptedKey == "" {
return key.Encrypt(dataKey)
@@ -188,81 +159,53 @@ func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error {
return nil
}
// EncryptedDataKey returns the encrypted data key this master key holds.
func (key *MasterKey) EncryptedDataKey() []byte {
return []byte(key.EncryptedKey)
}
// SetEncryptedDataKey sets the encrypted data key for this master key.
func (key *MasterKey) SetEncryptedDataKey(enc []byte) {
key.EncryptedKey = string(enc)
}
// Decrypt decrypts the EncryptedKey field with Vault Transit and returns the result.
func (key *MasterKey) Decrypt() ([]byte, error) {
fullPath := path.Join(key.EnginePath, "decrypt", key.KeyName)
cli, err := vaultClient(key.VaultAddress)
fullPath := key.decryptPath()
client, err := vaultClient(key.VaultAddress, key.token)
if err != nil {
log.WithError(err).WithField("Path", fullPath).Error("Decryption failed")
return nil, err
}
payload := make(map[string]interface{})
payload["ciphertext"] = key.EncryptedKey
raw, err := cli.Logical().Write(fullPath, payload)
secret, err := client.Logical().Write(fullPath, decryptPayload(key.EncryptedKey))
if err != nil {
log.WithField("Path", fullPath).Info("Encryption failed")
return nil, err
log.WithError(err).WithField("Path", fullPath).Error("Decryption failed")
return nil, fmt.Errorf("failed to decrypt sops data key from Vault transit backend '%s': %w", fullPath, err)
}
if raw == nil || raw.Data == nil {
return nil, fmt.Errorf("The transit backend %s is empty", fullPath)
}
decrypted, ok := raw.Data["plaintext"]
if ok != true {
return nil, fmt.Errorf("there's no decrypted data")
}
dataKey, ok := decrypted.(string)
if ok != true {
return nil, fmt.Errorf("the plaintest cannot be casted to string")
}
result, err := base64.StdEncoding.DecodeString(dataKey)
dataKey, err := dataKeyFromSecret(secret)
if err != nil {
return nil, fmt.Errorf("Couldn't decode base64 plaintext")
log.WithError(err).WithField("Path", fullPath).Error("Decryption failed")
return nil, fmt.Errorf("failed to decrypt sops data key from Vault transit backend '%s': %w", fullPath, err)
}
return result, nil
log.WithField("Path", fullPath).Info("Decryption successful")
return dataKey, nil
}
// NeedsRotation returns whether the data key needs to be rotated or not.
// This is simply copied from GCPKMS
// TODO: handle key rotation on vault side
func (key *MasterKey) NeedsRotation() bool {
//TODO: manage rewrapping https://www.vaultproject.io/api/secret/transit/index.html#rewrap-data
return time.Since(key.CreationDate) > (time.Hour * 24 * 30 * 6)
// TODO: manage rewrapping https://www.vaultproject.io/api/secret/transit/index.html#rewrap-data
return time.Since(key.CreationDate) > (vaultTTL)
}
// ToString converts the key to a string representation
// ToString converts the key to a string representation.
func (key *MasterKey) ToString() string {
return fmt.Sprintf("%s/v1/%s/keys/%s", key.VaultAddress, key.EnginePath, key.KeyName)
}
func (key *MasterKey) createVaultTransitAndKey() error {
cli, err := vaultClient(key.VaultAddress)
if err != nil {
return err
}
if err != nil {
return fmt.Errorf("Cannot create Vault Client: %w", err)
}
err = cli.Sys().Mount(key.EnginePath, &api.MountInput{
Type: "transit",
Description: "backend transit used by SOPS",
})
if err != nil {
return err
}
path := path.Join(key.EnginePath, "keys", key.KeyName)
payload := make(map[string]interface{})
payload["type"] = "rsa-4096"
_, err = cli.Logical().Write(path, payload)
if err != nil {
return err
}
_, err = cli.Logical().Read(path)
if err != nil {
return err
}
return nil
}
// ToMap converts the MasterKey to a map for serialization purposes
// ToMap converts the MasterKey to a map for serialization purposes.
func (key MasterKey) ToMap() map[string]interface{} {
out := make(map[string]interface{})
out["vault_address"] = key.VaultAddress
@@ -272,3 +215,145 @@ func (key MasterKey) ToMap() map[string]interface{} {
out["created_at"] = key.CreationDate.UTC().Format(time.RFC3339)
return out
}
// encryptPath returns the path for Encrypt requests.
func (key *MasterKey) encryptPath() string {
return path.Join(key.EnginePath, "encrypt", key.KeyName)
}
// decryptPath returns the path for Decrypt requests.
func (key *MasterKey) decryptPath() string {
return path.Join(key.EnginePath, "decrypt", key.KeyName)
}
// encryptPayload returns the payload for an encrypt request of the dataKey.
func encryptPayload(dataKey []byte) map[string]interface{} {
encoded := base64.StdEncoding.EncodeToString(dataKey)
return map[string]interface{}{
"plaintext": encoded,
}
}
// encryptedKeyFromSecret attempts to extract the encrypted key from the data
// of the provided secret.
func encryptedKeyFromSecret(secret *api.Secret) (string, error) {
if secret == nil || secret.Data == nil {
return "", fmt.Errorf("transit backend is empty")
}
encrypted, ok := secret.Data["ciphertext"]
if !ok {
return "", fmt.Errorf("no encrypted data")
}
encryptedKey, ok := encrypted.(string)
if !ok {
return "", fmt.Errorf("encrypted ciphertext cannot be cast to string")
}
return encryptedKey, nil
}
// decryptPayload returns the payload for a decrypt request of the
// encryptedKey.
func decryptPayload(encryptedKey string) map[string]interface{} {
return map[string]interface{}{
"ciphertext": encryptedKey,
}
}
// dataKeyFromSecret attempts to extract the data key from the data of the
// provided secret.
func dataKeyFromSecret(secret *api.Secret) ([]byte, error) {
if secret == nil || secret.Data == nil {
return nil, fmt.Errorf("transit backend is empty")
}
decrypted, ok := secret.Data["plaintext"]
if !ok {
return nil, fmt.Errorf("no decrypted data")
}
plaintext, ok := decrypted.(string)
if !ok {
return nil, fmt.Errorf("decrypted plaintext data cannot be cast to string")
}
dataKey, err := base64.StdEncoding.DecodeString(plaintext)
if err != nil {
return nil, fmt.Errorf("cannot decode base64 plaintext into data key bytes")
}
return dataKey, nil
}
// vaultClient returns a new Vault client, configured with the given address
// and token.
func vaultClient(address, token string) (*api.Client, error) {
cfg := api.DefaultConfig()
cfg.Address = address
client, err := api.NewClient(cfg)
if err != nil {
return nil, fmt.Errorf("cannot create Vault client: %w", err)
}
if token != "" {
client.SetToken(token)
}
// Provided token takes precedence over the user's token.
if client.Token() == "" {
if token, err = userVaultToken(); err != nil {
return nil, fmt.Errorf("cannot get Vault token: %w", err)
}
if token != "" {
client.SetToken(token)
}
}
return client, nil
}
// userVaultsToken returns the token from `$HOME/.vault-token` if the file
// exists. It returns an error if the file exists but cannot be read from.
// If the file does not exist, it returns an empty string.
func userVaultToken() (string, error) {
homePath, err := homedir.Dir()
if err != nil {
return "", fmt.Errorf("error getting user's home directory: %w", err)
}
tokenPath := filepath.Join(homePath, defaultTokenFile)
f, err := os.Open(tokenPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return "", nil
}
return "", err
}
defer f.Close()
buf := bytes.NewBuffer(nil)
if _, err := io.Copy(buf, f); err != nil {
return "", err
}
return strings.TrimSpace(buf.String()), nil
}
// engineAndKeyFromPath returns the engine path and key name from the full
// path, or an error.
func engineAndKeyFromPath(fullPath string) (enginePath, keyName string, err error) {
// Running vault behind a reverse proxy with longer URLs seems not to be
// supported by the Vault client API. Check for this here.
// TODO(hidde): this may no longer be necessary with newer Vault versions,
// but needs to be confirmed.
if re := regexp.MustCompile(`/[^/]+/v[\d]+/[^/]+/[^/]+/[^/]+`); re.Match([]byte(fullPath)) {
err = fmt.Errorf("running Vault with a prefixed URL is not supported! (Format has to be like " +
"https://vault.example.com:8200/v1/transit/keys/keyName)")
return
} else if re := regexp.MustCompile(`/v[\d]+/[^/]+/[^/]+/[^/]+`); !re.Match([]byte(fullPath)) {
err = fmt.Errorf("vault path does not seem to be formatted correctly: (eg. " +
"https://vault.example.com:8200/v1/transit/keys/keyName)")
return
}
fullPath = strings.Trim(fullPath, "/")
dirs := strings.Split(fullPath, "/")
keyName = dirs[len(dirs)-1]
enginePath = path.Join(dirs[1 : len(dirs)-2]...)
return
}

View File

@@ -4,166 +4,511 @@ import (
"fmt"
logger "log"
"os"
"path"
"path/filepath"
"strings"
"testing"
"time"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/go-homedir"
"github.com/ory/dockertest"
"github.com/stretchr/testify/assert"
)
var (
// testVaultVersion is the version (image tag) of the Vault server image
// used to test against.
testVaultVersion = "1.10.0"
// testVaultToken is the token of the Vault server.
testVaultToken = "secret"
// testEnginePath is the path to mount the Vault Transit on.
testEnginePath = "sops"
// testVaultAddress is the HTTP/S address of the Vault server, it is set
// by TestMain after booting it.
testVaultAddress string
)
// TestMain initializes a Vault server using Docker, writes the HTTP address to
// testVaultAddress, waits for it to become ready to serve requests, and enables
// Vault Transit on the testEnginePath. It then runs all the tests, which can
// make use of the various `test*` variables.
func TestMain(m *testing.M) {
// uses a sensible default on windows (tcp/http) and linux/osx (socket)
// Uses a sensible default on Windows (TCP/HTTP) and Linux/MacOS (socket)
pool, err := dockertest.NewPool("")
if err != nil {
logger.Fatalf("Could not connect to docker: %s", err)
logger.Fatalf("could not connect to docker: %s", err)
}
// pulls an image, creates a container based on it and runs it
resource, err := pool.Run("vault", "1.2.2", []string{"VAULT_DEV_ROOT_TOKEN_ID=secret"})
// Pull the image, create a container based on it, and run it
resource, err := pool.Run("vault", testVaultVersion, []string{"VAULT_DEV_ROOT_TOKEN_ID=" + testVaultToken})
if err != nil {
logger.Fatalf("Could not start resource: %s", err)
logger.Fatalf("could not start resource: %s", err)
}
vaultAddr := fmt.Sprintf("http://%s", resource.GetHostPort("8200/tcp"))
os.Setenv("VAULT_ADDR", vaultAddr)
os.Setenv("VAULT_TOKEN", "secret")
// exponential backoff-retry, because the application in the container might not be ready to accept connections yet
purgeResource := func() {
if err := pool.Purge(resource); err != nil {
logger.Printf("could not purge resource: %s", err)
}
}
testVaultAddress = fmt.Sprintf("http://127.0.0.1:%v", resource.GetPort("8200/tcp"))
// Wait until Vault is ready to serve requests
if err := pool.Retry(func() error {
cli, err := api.NewClient(api.DefaultConfig())
cfg := api.DefaultConfig()
cfg.Address = testVaultAddress
cli, err := api.NewClient(cfg)
if err != nil {
return fmt.Errorf("Cannot create Vault Client: %w", err)
return fmt.Errorf("cannot create Vault client: %w", err)
}
status, err := cli.Sys().InitStatus()
if err != nil {
return err
}
if status != true {
return fmt.Errorf("Vault not ready yet")
return fmt.Errorf("waiting on Vault server to become ready")
}
return nil
}); err != nil {
logger.Fatalf("Could not connect to docker: %s", err)
purgeResource()
logger.Fatalf("could not connect to docker: %s", err)
}
key := NewMasterKey(vaultAddr, "sops", "main")
err = key.createVaultTransitAndKey()
if err != nil {
logger.Fatal(err)
if err = enableVaultTransit(testVaultAddress, testVaultToken, testEnginePath); err != nil {
purgeResource()
logger.Fatalf("could not enable Vault transit: %s", err)
}
code := 0
// Run the tests, but only if we succeeded in setting up the Vault server
var code int
if err == nil {
code = m.Run()
}
// You can't defer this because os.Exit doesn't care for defer
// This can't be deferred, as os.Exit simpy does not care
if err := pool.Purge(resource); err != nil {
logger.Fatalf("Could not purge resource: %s", err)
logger.Fatalf("could not purge resource: %s", err)
}
os.Exit(code)
}
func TestKeyToMap(t *testing.T) {
key := MasterKey{
CreationDate: time.Date(2016, time.October, 31, 10, 0, 0, 0, time.UTC),
VaultAddress: "http://127.0.0.1:8200",
EnginePath: "foo",
KeyName: "bar",
EncryptedKey: "this is encrypted",
}
assert.Equal(t, map[string]interface{}{
"vault_address": "http://127.0.0.1:8200",
"engine_path": "foo",
"key_name": "bar",
"enc": "this is encrypted",
"created_at": "2016-10-31T10:00:00Z",
}, key.ToMap())
}
func TestNewMasterKeysFromURIs(t *testing.T) {
t.Run("multiple URIs", func(t *testing.T) {
uris := []string{
"https://vault.example.com:8200/v1/transit/keys/keyName",
"", // Empty should be skipped
"https://vault.me.com/v1/super42/bestmarket/keys/slig",
}
keys, err := NewMasterKeysFromURIs(strings.Join(uris, ","))
assert.NoError(t, err)
assert.Len(t, keys, 2)
})
func TestEncryptionDecryption(t *testing.T) {
dataKey := []byte("super very Secret Key!!!")
key := MasterKey{
VaultAddress: os.Getenv("VAULT_ADDR"),
EnginePath: "sops",
KeyName: "main",
}
err := key.Encrypt(dataKey)
if err != nil {
fmt.Println(err)
t.Fail()
return
}
decrypted, err := key.Decrypt()
if err != nil {
fmt.Println(err)
t.Fail()
return
}
assert.Equal(t, dataKey, decrypted)
t.Run("with invalid URI", func(t *testing.T) {
uris := []string{
"https://vault.example.com:8200/v1/transit/keys/keyName",
"vault.me/keys/dev/mykey",
}
keys, err := NewMasterKeysFromURIs(strings.Join(uris, ","))
assert.Error(t, err)
assert.Nil(t, keys)
})
}
func TestNewMasterKeyFromURI(t *testing.T) {
uri1 := "https://vault.example.com:8200/v1/transit/keys/keyName"
uri2 := "https://vault.me.com/v1/super42/bestmarket/keys/slig"
uri3 := "http://127.0.0.1:12121/v1/transit/keys/dev"
mk1 := &MasterKey{
VaultAddress: "https://vault.example.com:8200",
EnginePath: "transit",
KeyName: "keyName",
tests := []struct {
url string
want *MasterKey
wantErr bool
}{
{
url: "https://vault.example.com:8200/v1/transit/keys/keyName",
want: &MasterKey{
VaultAddress: "https://vault.example.com:8200",
EnginePath: "transit",
KeyName: "keyName",
},
},
{
url: "https://vault.me.com/v1/super42/bestmarket/keys/slig",
want: &MasterKey{
VaultAddress: "https://vault.me.com",
EnginePath: "super42/bestmarket",
KeyName: "slig",
},
},
{
url: "http://127.0.0.1:12121/v1/transit/keys/dev",
want: &MasterKey{
VaultAddress: "http://127.0.0.1:12121",
EnginePath: "transit",
KeyName: "dev",
},
},
{
url: "vault.me/keys/dev/mykey",
want: nil,
wantErr: true,
},
{
url: "http://127.0.0.1:12121/v1/keys/dev",
want: nil,
wantErr: true,
},
{
url: "tcp://127.0.0.1:12121/v1/keys/dev",
want: nil,
wantErr: true,
},
}
mk2 := &MasterKey{
VaultAddress: "https://vault.me.com",
EnginePath: "super42/bestmarket",
KeyName: "slig",
for _, tt := range tests {
t.Run(tt.url, func(t *testing.T) {
got, err := NewMasterKeyFromURI(tt.url)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
if tt.want != nil && got != nil {
tt.want.CreationDate = got.CreationDate
}
assert.Equal(t, tt.want, got)
})
}
mk3 := &MasterKey{
VaultAddress: "http://127.0.0.1:12121",
EnginePath: "transit",
KeyName: "dev",
}
genMk1, err := NewMasterKeyFromURI(uri1)
if err != nil {
log.Errorln(err)
t.Fail()
}
genMk2, err := NewMasterKeyFromURI(uri2)
if err != nil {
log.Errorln(err)
t.Fail()
}
genMk3, err := NewMasterKeyFromURI(uri3)
if err != nil {
log.Errorln(err)
t.Fail()
}
if assert.NotNil(t, genMk1) {
mk1.CreationDate = genMk1.CreationDate
assert.Equal(t, mk1, genMk1)
}
if assert.NotNil(t, genMk2) {
mk2.CreationDate = genMk2.CreationDate
assert.Equal(t, mk2, genMk2)
}
if assert.NotNil(t, genMk3) {
mk3.CreationDate = genMk3.CreationDate
assert.Equal(t, mk3, genMk3)
}
badURIs := []string{
"vault.me/keys/dev/mykey",
"http://127.0.0.1:12121/v1/keys/dev",
"tcp://127.0.0.1:12121/v1/keys/dev",
}
for _, uri := range badURIs {
if _, err = NewMasterKeyFromURI(uri); err == nil {
log.Errorf("Should be a invalid uri: %s", uri)
t.Fail()
}
}
}
func TestMasterKey_Encrypt(t *testing.T) {
key := NewMasterKey(testVaultAddress, testEnginePath, "encrypt")
(Token(testVaultToken)).ApplyToMasterKey(key)
assert.NoError(t, createVaultKey(key))
dataKey := []byte("the majority of your brain is fat")
assert.NoError(t, key.Encrypt(dataKey))
assert.NotEmpty(t, key.EncryptedKey)
client, err := vaultClient(key.VaultAddress, key.token)
assert.NoError(t, err)
payload := decryptPayload(key.EncryptedKey)
secret, err := client.Logical().Write(key.decryptPath(), payload)
assert.NoError(t, err)
decryptedData, err := dataKeyFromSecret(secret)
assert.NoError(t, err)
assert.Equal(t, dataKey, decryptedData)
key.EnginePath = "invalid"
assert.Error(t, key.Encrypt(dataKey))
key.EnginePath = testEnginePath
key.token = ""
assert.Error(t, key.Encrypt(dataKey))
}
func TestMasterKey_EncryptIfNeeded(t *testing.T) {
key := NewMasterKey(testVaultAddress, testEnginePath, "encrypt-if-needed")
(Token(testVaultToken)).ApplyToMasterKey(key)
assert.NoError(t, createVaultKey(key))
assert.NoError(t, key.EncryptIfNeeded([]byte("stingy string")))
encryptedKey := key.EncryptedKey
assert.NotEmpty(t, encryptedKey)
assert.NoError(t, key.EncryptIfNeeded([]byte("stringy sting")))
assert.Equal(t, encryptedKey, key.EncryptedKey)
}
func TestMasterKey_EncryptedDataKey(t *testing.T) {
key := &MasterKey{EncryptedKey: "some key"}
assert.EqualValues(t, key.EncryptedKey, key.EncryptedDataKey())
}
func TestMasterKey_Decrypt(t *testing.T) {
key := NewMasterKey(testVaultAddress, testEnginePath, "decrypt")
(Token(testVaultToken)).ApplyToMasterKey(key)
assert.NoError(t, createVaultKey(key))
client, err := vaultClient(key.VaultAddress, key.token)
assert.NoError(t, err)
dataKey := []byte("the heart of a shrimp is located in its head")
secret, err := client.Logical().Write(key.encryptPath(), encryptPayload(dataKey))
assert.NoError(t, err)
encryptedKey, err := encryptedKeyFromSecret(secret)
assert.NoError(t, err)
key.EncryptedKey = encryptedKey
got, err := key.Decrypt()
assert.NoError(t, err)
assert.Equal(t, dataKey, got)
key.EnginePath = "invalid"
assert.Error(t, key.Encrypt(dataKey))
key.EnginePath = testEnginePath
key.token = ""
assert.Error(t, key.Encrypt(dataKey))
}
func TestMasterKey_EncryptDecrypt_RoundTrip(t *testing.T) {
token := Token(testVaultToken)
encryptKey := NewMasterKey(testVaultAddress, testEnginePath, "roundtrip")
token.ApplyToMasterKey(encryptKey)
assert.NoError(t, createVaultKey(encryptKey))
dataKey := []byte("some people have an extra bone in their knee")
assert.NoError(t, encryptKey.Encrypt(dataKey))
assert.NotEmpty(t, encryptKey.EncryptedKey)
decryptKey := NewMasterKey(testVaultAddress, testEnginePath, "roundtrip")
token.ApplyToMasterKey(decryptKey)
decryptKey.EncryptedKey = encryptKey.EncryptedKey
decryptedData, err := decryptKey.Decrypt()
assert.NoError(t, err)
assert.Equal(t, dataKey, decryptedData)
}
func TestMasterKey_NeedsRotation(t *testing.T) {
key := NewMasterKey("", "", "")
assert.False(t, key.NeedsRotation())
key.CreationDate = key.CreationDate.Add(-(vaultTTL + time.Second))
assert.True(t, key.NeedsRotation())
}
func TestMasterKey_ToString(t *testing.T) {
key := NewMasterKey("https://example.com", "engine", "key-name")
assert.Equal(t, "https://example.com/v1/engine/keys/key-name", key.ToString())
}
func TestMasterKey_ToMap(t *testing.T) {
key := &MasterKey{
KeyName: "test-key",
EnginePath: "engine",
VaultAddress: testVaultAddress,
EncryptedKey: "some-encrypted-key",
}
assert.Equal(t, map[string]interface{}{
"vault_address": key.VaultAddress,
"key_name": key.KeyName,
"engine_path": key.EnginePath,
"enc": key.EncryptedKey,
"created_at": "0001-01-01T00:00:00Z",
}, key.ToMap())
}
func Test_encryptedKeyFromSecret(t *testing.T) {
tests := []struct {
name string
secret *api.Secret
want string
wantErr bool
}{
{name: "nil secret", secret: nil, wantErr: true},
{name: "secret with nil data", secret: &api.Secret{Data: nil}, wantErr: true},
{name: "secret without ciphertext data", secret: &api.Secret{Data: map[string]interface{}{"other": true}}, wantErr: true},
{name: "ciphertext non string", secret: &api.Secret{Data: map[string]interface{}{"ciphertext": 123}}, wantErr: true},
{name: "ciphertext data", secret: &api.Secret{Data: map[string]interface{}{"ciphertext": "secret string"}}, want: "secret string"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := encryptedKeyFromSecret(tt.secret)
if tt.wantErr {
assert.Error(t, err)
assert.Empty(t, got)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func Test_dataKeyFromSecret(t *testing.T) {
tests := []struct {
name string
secret *api.Secret
want []byte
wantErr bool
}{
{name: "nil secret", secret: nil, wantErr: true},
{name: "secret with nil data", secret: &api.Secret{Data: nil}, wantErr: true},
{name: "secret without plaintext data", secret: &api.Secret{Data: map[string]interface{}{"other": true}}, wantErr: true},
{name: "plaintext non string", secret: &api.Secret{Data: map[string]interface{}{"plaintext": 123}}, wantErr: true},
{name: "plaintext non base64", secret: &api.Secret{Data: map[string]interface{}{"plaintext": "notbase64"}}, wantErr: true},
{name: "plaintext base64 data", secret: &api.Secret{Data: map[string]interface{}{"plaintext": "Zm9v"}}, want: []byte("foo")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := dataKeyFromSecret(tt.secret)
if tt.wantErr {
assert.Error(t, err)
assert.Empty(t, got)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func Test_vaultClient(t *testing.T) {
t.Run("client", func(t *testing.T) {
t.Setenv("VAULT_TOKEN", "")
got, err := vaultClient(testVaultAddress, "")
assert.NoError(t, err)
assert.NotNil(t, got)
assert.Empty(t, got.Token())
})
t.Run("client with VAULT_TOKEN", func(t *testing.T) {
token := "test-token"
t.Setenv("VAULT_TOKEN", token)
got, err := vaultClient(testVaultAddress, "")
assert.NoError(t, err)
assert.NotNil(t, got)
assert.Equal(t, token, got.Token())
})
t.Run("client with token", func(t *testing.T) {
ignored := "test-token"
t.Setenv("VAULT_TOKEN", ignored)
got, err := vaultClient(testVaultAddress, testVaultToken)
assert.NoError(t, err)
assert.NotNil(t, got)
assert.Equal(t, testVaultToken, got.Token())
})
t.Run("client with token from file", func(t *testing.T) {
tmpDir := t.TempDir()
token := "test-token"
assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, defaultTokenFile), []byte(token), 0600))
// Reset before and after to make sure the override is taken into
// account, and restored after the test.
homedir.Reset()
t.Cleanup(func() { homedir.Reset() })
t.Setenv("VAULT_TOKEN", "")
t.Setenv("HOME", tmpDir)
got, err := vaultClient(testVaultAddress, "")
assert.NoError(t, err)
assert.NotNil(t, got)
assert.Equal(t, token, got.Token())
})
}
func Test_userVaultToken(t *testing.T) {
t.Run("reads token from file", func(t *testing.T) {
tmpDir := t.TempDir()
token := "test-token"
assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, defaultTokenFile), []byte(token), 0600))
// Reset before and after to make sure the override is taken into
// account, and restored after the test.
homedir.Reset()
t.Cleanup(func() { homedir.Reset() })
t.Setenv("HOME", tmpDir)
got, err := userVaultToken()
assert.NoError(t, err)
assert.Equal(t, token, got)
})
t.Run("ignores missing file", func(t *testing.T) {
tmpDir := t.TempDir()
// Reset before and after to make sure the override is taken into
// account, and restored after the test.
homedir.Reset()
t.Cleanup(func() { homedir.Reset() })
t.Setenv("HOME", tmpDir)
got, err := userVaultToken()
assert.NoError(t, err)
assert.Empty(t, got)
})
t.Run("trims spaces", func(t *testing.T) {
tmpDir := t.TempDir()
token := " test-token "
assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, defaultTokenFile), []byte(token), 0600))
// Reset before and after to make sure the override is taken into
// account, and restored after the test.
homedir.Reset()
t.Cleanup(func() { homedir.Reset() })
t.Setenv("HOME", tmpDir)
got, err := userVaultToken()
assert.NoError(t, err)
assert.Equal(t, "test-token", got)
})
}
func Test_engineAndKeyFromPath(t *testing.T) {
t.Run("engine and key", func(t *testing.T) {
enginePath, key, err := engineAndKeyFromPath("/v1/transit/keys/keyName")
assert.NoError(t, err)
assert.Equal(t, "transit", enginePath)
assert.Equal(t, "keyName", key)
})
t.Run("long (nested) path error", func(t *testing.T) {
_, _, err := engineAndKeyFromPath("/nested/v1/transit/keys/bar")
assert.Error(t, err)
assert.ErrorContains(t, err, "running Vault with a prefixed URL is not supported")
})
t.Run("invalid format error", func(t *testing.T) {
_, _, err := engineAndKeyFromPath("/secret/foo/bar")
assert.Error(t, err)
assert.ErrorContains(t, err, "vault path does not seem to be formatted correctly")
})
}
// enableVaultTransit enables the Vault Transit backend on the given enginePath.
func enableVaultTransit(address, token, enginePath string) error {
client, err := vaultClient(address, token)
if err != nil {
return fmt.Errorf("cannot create Vault client: %w", err)
}
if err = client.Sys().Mount(enginePath, &api.MountInput{
Type: "transit",
Description: "backend transit used by SOPS",
}); err != nil {
return fmt.Errorf("failed to mount transit on engine path '%s': %w", enginePath, err)
}
return nil
}
// createVaultKey creates a new RSA-4096 Vault key using the data from the
// provided MasterKey.
func createVaultKey(key *MasterKey) error {
client, err := vaultClient(key.VaultAddress, key.token)
if err != nil {
return fmt.Errorf("cannot create Vault client: %w", err)
}
p := path.Join(key.EnginePath, "keys", key.KeyName)
payload := make(map[string]interface{})
payload["type"] = "rsa-4096"
if _, err = client.Logical().Write(p, payload); err != nil {
return err
}
_, err = client.Logical().Read(p)
return err
}