add OpenBao as key provider for state encryption (#1436)

Signed-off-by: ollevche <ollevche@gmail.com>
This commit is contained in:
Oleksandr Levchenkov 2024-04-08 15:38:17 +03:00 committed by GitHub
parent 0b90cc3383
commit e1e182987b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 624 additions and 12 deletions

15
go.mod
View File

@ -70,6 +70,7 @@ require (
github.com/mitchellh/gox v1.0.1
github.com/mitchellh/reflectwalk v1.0.2
github.com/nishanths/exhaustive v0.7.11
github.com/openbao/openbao/api v0.0.0-20240326035453-c075f0ef2c7e
github.com/opentofu/registry-address v0.0.0-20230920144404-f1e51167f633
github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db
github.com/pkg/errors v0.9.1
@ -89,13 +90,13 @@ require (
go.opentelemetry.io/otel v1.21.0
go.opentelemetry.io/otel/sdk v1.21.0
go.opentelemetry.io/otel/trace v1.21.0
golang.org/x/crypto v0.18.0
golang.org/x/crypto v0.21.0
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
golang.org/x/mod v0.12.0
golang.org/x/net v0.20.0
golang.org/x/net v0.21.0
golang.org/x/oauth2 v0.16.0
golang.org/x/sys v0.16.0
golang.org/x/term v0.16.0
golang.org/x/sys v0.18.0
golang.org/x/term v0.18.0
golang.org/x/text v0.14.0
golang.org/x/tools v0.13.0
google.golang.org/api v0.155.0
@ -157,6 +158,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.25.6 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/bradleyfalzon/ghinstallation/v2 v2.1.0 // indirect
github.com/cenkalti/backoff/v3 v3.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cli/go-gh v1.0.0 // indirect
github.com/cli/safeexec v1.0.0 // indirect
@ -168,6 +170,7 @@ require (
github.com/fatih/color v1.15.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/errors v0.20.2 // indirect
@ -188,7 +191,10 @@ require (
github.com/hashicorp/go-msgpack v0.5.4 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // 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-slug v0.12.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
github.com/hashicorp/golang-lru v0.5.1 // indirect
github.com/hashicorp/serf v0.9.6 // indirect
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
@ -222,6 +228,7 @@ require (
github.com/oklog/run v1.0.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/samber/lo v1.37.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect

30
go.sum
View File

@ -370,6 +370,8 @@ github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTS
github.com/bradleyfalzon/ghinstallation/v2 v2.1.0 h1:5+NghM1Zred9Z078QEZtm28G/kfDfZN/92gkDlLwGVA=
github.com/bradleyfalzon/ghinstallation/v2 v2.1.0/go.mod h1:Xg3xPRN5Mcq6GDqeUVhFbjEWMb4JHCyWEeeBGEYQoTU=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c=
github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@ -464,6 +466,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
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-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
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-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@ -684,6 +688,11 @@ github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5O
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo=
github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
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-slug v0.12.2 h1:Gb6nxnV5GI1UVa3aLJGUj66J8AOZFnjIoYalNCp2Cbo=
github.com/hashicorp/go-slug v0.12.2/go.mod h1:JZVtycnZZbiJ4oxpJ/zfhyfBD8XxT4f0uOSyjNLCqFY=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
@ -869,6 +878,7 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
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.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
@ -915,6 +925,8 @@ github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGV
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/openbao/openbao/api v0.0.0-20240326035453-c075f0ef2c7e h1:LIQFfqW6BA5E2ycx8NNDgyKh0exFubHePM5pF3knogo=
github.com/openbao/openbao/api v0.0.0-20240326035453-c075f0ef2c7e/go.mod h1:NUvBdXCNlmAGQ9TbYV7vS1Y9awHAjrq3QLiBWV+4Glk=
github.com/opentofu/registry-address v0.0.0-20230920144404-f1e51167f633 h1:81TBkM/XGIFlVvyabp0CJl00UHeVUiQjz0fddLMi848=
github.com/opentofu/registry-address v0.0.0-20230920144404-f1e51167f633/go.mod h1:HzQhpVo/NJnGmN+7FPECCVCA5ijU7AUcvf39enBKYOc=
github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db h1:9uViuKtx1jrlXLBW/pMnhOfzn3iSEdLase/But/IZRU=
@ -963,6 +975,7 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw=
github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA=
@ -1106,6 +1119,7 @@ golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -1122,8 +1136,8 @@ golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -1232,8 +1246,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1379,8 +1393,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -1389,8 +1403,8 @@ golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -8,6 +8,7 @@ package encryption
import (
"github.com/opentofu/opentofu/internal/encryption/keyprovider/aws_kms"
"github.com/opentofu/opentofu/internal/encryption/keyprovider/gcp_kms"
"github.com/opentofu/opentofu/internal/encryption/keyprovider/openbao"
"github.com/opentofu/opentofu/internal/encryption/keyprovider/pbkdf2"
"github.com/opentofu/opentofu/internal/encryption/method/aesgcm"
"github.com/opentofu/opentofu/internal/encryption/registry/lockingencryptionregistry"
@ -28,4 +29,7 @@ func init() {
if err := DefaultRegistry.RegisterKeyProvider(gcp_kms.New()); err != nil {
panic(err)
}
if err := DefaultRegistry.RegisterKeyProvider(openbao.New()); err != nil {
panic(err)
}
}

View File

@ -0,0 +1,88 @@
package openbao
import (
"context"
"encoding/base64"
"errors"
"fmt"
"net/url"
"path"
openbao "github.com/openbao/openbao/api"
)
type client interface {
WriteWithContext(ctx context.Context, path string, data map[string]interface{}) (*openbao.Secret, error)
}
// service implements missing utility functions from openbao/api such as routing and serialization.
type service struct {
c client
transitPath string
}
type dataKey struct {
Plaintext []byte
Ciphertext []byte
}
func (s service) generateDataKey(ctx context.Context, keyName string, bitSize int) (dataKey, error) {
path := path.Join(s.transitPath, "datakey/plaintext", url.PathEscape(keyName))
secret, err := s.c.WriteWithContext(ctx, path, map[string]interface{}{
"bits": bitSize,
})
if err != nil {
return dataKey{}, fmt.Errorf("error sending datakey request to OpenBao: %w", err)
}
key := dataKey{}
key.Ciphertext, err = retrieveCiphertext(secret)
if err != nil {
return dataKey{}, err
}
key.Plaintext, err = retrievePlaintext(secret)
if err != nil {
return dataKey{}, err
}
return key, nil
}
func (s service) decryptData(ctx context.Context, keyName string, ciphertext []byte) ([]byte, error) {
path := path.Join(s.transitPath, "decrypt", url.PathEscape(keyName))
secret, err := s.c.WriteWithContext(ctx, path, map[string]interface{}{
"ciphertext": string(ciphertext),
})
if err != nil {
return nil, fmt.Errorf("error sending decryption request to OpenBao: %w", err)
}
return retrievePlaintext(secret)
}
func retrievePlaintext(s *openbao.Secret) ([]byte, error) {
base64Plaintext, ok := s.Data["plaintext"].(string)
if !ok {
return nil, errors.New("failed to deserialize 'plaintext' (it's either OpenTofu bug or incompatible OpenBao version)")
}
plaintext, err := base64.StdEncoding.DecodeString(base64Plaintext)
if err != nil {
return nil, fmt.Errorf("base64 decoding 'plaintext' (it's either OpenTofu bug or incompatible OpenBao version): %w", err)
}
return plaintext, nil
}
func retrieveCiphertext(s *openbao.Secret) ([]byte, error) {
ciphertext, ok := s.Data["ciphertext"].(string)
if !ok {
return nil, errors.New("failed to deserialize 'ciphertext' (it's either OpenTofu bug or incompatible OpenBao version)")
}
return []byte(ciphertext), nil
}

View File

@ -0,0 +1,242 @@
package openbao
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"net/url"
"os"
"testing"
openbao "github.com/openbao/openbao/api"
"github.com/opentofu/opentofu/internal/encryption/keyprovider/compliancetest"
)
func getBaoKeyName() string {
// Acceptance tests are disabled, running with mock.
if os.Getenv("TF_ACC") == "" {
return ""
}
return os.Getenv("TF_ACC_BAO_KEY_NAME")
}
const defaultTestKeyName = "test-key"
func TestKeyProvider(t *testing.T) {
testKeyName := getBaoKeyName()
if testKeyName == "" {
testKeyName = defaultTestKeyName
mock := prepareClientMockForKeyProviderTest(t, testKeyName)
injectMock(mock)
t.Cleanup(func() {
injectDefaultClient()
})
}
compliancetest.ComplianceTest(
t,
compliancetest.TestConfiguration[*descriptor, *Config, *keyMeta, *keyProvider]{
Descriptor: New().(*descriptor),
HCLParseTestCases: map[string]compliancetest.HCLParseTestCase[*Config, *keyProvider]{
"success": {
HCL: fmt.Sprintf(`key_provider "openbao" "foo" {
key_name = "%s"
}`, testKeyName),
ValidHCL: true,
ValidBuild: true,
},
"success-full-creds": {
HCL: fmt.Sprintf(`key_provider "openbao" "foo" {
token = "s.dummytoken"
address = "http://127.0.0.1:8201"
key_name = "%s"
}`, testKeyName),
ValidHCL: true,
ValidBuild: true,
},
"empty": {
HCL: `key_provider "openbao" "foo" {}`,
ValidHCL: false,
ValidBuild: false,
},
"empty-key-name": {
HCL: `key_provider "openbao" "foo" {
key_name = ""
}`,
ValidHCL: true,
ValidBuild: false,
},
"invalid-key-length": {
HCL: fmt.Sprintf(`key_provider "openbao" "foo" {
key_name = "%s"
key_length = 17
}`, testKeyName),
ValidHCL: true,
ValidBuild: false,
},
"no-key-name": {
HCL: `key_provider "openbao" "foo" {
key_length = 16
}`,
ValidHCL: false,
ValidBuild: false,
},
"unknown-property": {
HCL: fmt.Sprintf(`key_provider "openbao" "foo" {
key_name = "%s"
key_length = 16
unknown_property = "foo"
}`, testKeyName),
ValidHCL: false,
ValidBuild: false,
},
"transit-path": {
HCL: fmt.Sprintf(`key_provider "openbao" "foo" {
key_name = "%s"
key_length = 16
transit_engine_path = "foo"
}`, testKeyName),
ValidHCL: true,
ValidBuild: true,
},
},
ConfigStructTestCases: map[string]compliancetest.ConfigStructTestCase[*Config, *keyProvider]{
"success": {
Config: &Config{
KeyName: testKeyName,
KeyLength: 16,
TransitEnginePath: "/pki",
},
ValidBuild: true,
Validate: func(p *keyProvider) error {
if p.keyName != testKeyName {
return fmt.Errorf("key names don't match: %v and %v", p.keyName, testKeyName)
}
if p.keyLength != 16 {
return fmt.Errorf("invalid key length: %v", p.keyLength)
}
if p.svc.transitPath != "/pki" {
return fmt.Errorf("invalid transit path: %v", p.svc.transitPath)
}
return nil
},
},
"success-default-values": {
Config: &Config{
KeyName: testKeyName,
},
ValidBuild: true,
Validate: func(p *keyProvider) error {
if p.keyName != testKeyName {
return fmt.Errorf("key names don't match: %v and %v", p.keyName, testKeyName)
}
if p.keyLength != 32 {
return fmt.Errorf("invalid default key length: %v", p.keyLength)
}
if p.svc.transitPath != "/transit" {
return fmt.Errorf("invalid default transit path: %v; expected: '/transit'", p.svc.transitPath)
}
return nil
},
},
"empty": {
Config: &Config{},
ValidBuild: false,
Validate: nil,
},
},
MetadataStructTestCases: map[string]compliancetest.MetadataStructTestCase[*Config, *keyMeta]{
"empty": {
ValidConfig: &Config{
KeyName: testKeyName,
},
Meta: &keyMeta{},
IsPresent: false,
IsValid: false,
},
},
ProvideTestCase: compliancetest.ProvideTestCase[*Config, *keyMeta]{
ValidConfig: &Config{
KeyName: testKeyName,
},
ValidateKeys: func(dec []byte, enc []byte) error {
if len(dec) == 0 {
return fmt.Errorf("decryption key is empty")
}
if len(enc) == 0 {
return fmt.Errorf("encryption key is empty")
}
return nil
},
ValidateMetadata: func(meta *keyMeta) error {
if len(meta.Ciphertext) == 0 {
return fmt.Errorf("ciphertext is empty")
}
return nil
},
},
},
)
}
// Mocking is a bit complicated due to how openbao/api package is structured,
// but in order to test cover as much as we can, it has to have some logic in here.
func prepareClientMockForKeyProviderTest(t *testing.T, testKeyName string) mockClientFunc {
escapedTestKeyName := url.PathEscape(testKeyName)
// Mock uses default transit engine path: "/transit".
generateDataKeyPath := fmt.Sprintf("/transit/datakey/plaintext/%s", escapedTestKeyName)
decryptPath := fmt.Sprintf("/transit/decrypt/%s", escapedTestKeyName)
return func(ctx context.Context, path string, data map[string]interface{}) (*openbao.Secret, error) {
switch path {
case generateDataKeyPath:
bits, ok := data["bits"].(int)
if !ok {
t.Fatalf("Invalid bits in data suplied to mock: not a number")
}
plaintext := make([]byte, int(bits)/8)
if _, err := rand.Read(plaintext); err != nil {
panic(fmt.Errorf("generating random data key in mock: %w", err))
}
s := &openbao.Secret{
Data: map[string]interface{}{
"plaintext": base64.StdEncoding.EncodeToString(plaintext),
"ciphertext": string(append([]byte(testKeyName), plaintext...)),
},
}
return s, nil
case decryptPath:
ciphertext, ok := data["ciphertext"].(string)
if !ok {
t.Fatalf("Invalid ciphertext in data suplied to mock: not an string")
}
plaintext := []byte(ciphertext[len(testKeyName):])
s := &openbao.Secret{
Data: map[string]interface{}{
"plaintext": base64.StdEncoding.EncodeToString(plaintext),
},
}
return s, nil
default:
t.Fatalf("Invalid path suplied to mock: %s", path)
}
// unreachable code
return nil, nil
}
}

View File

@ -0,0 +1,110 @@
package openbao
import (
"fmt"
openbao "github.com/openbao/openbao/api"
"github.com/opentofu/opentofu/internal/encryption/keyprovider"
)
type Config struct {
Address string `hcl:"address,optional"`
Token string `hcl:"token,optional"`
KeyName string `hcl:"key_name"`
KeyLength DataKeyLength `hcl:"key_length,optional"`
TransitEnginePath string `hcl:"transit_engine_path,optional"`
}
const (
defaultDataKeyLength DataKeyLength = 32
defaultTransitEnginePath string = "/transit"
)
func (c Config) Build() (keyprovider.KeyProvider, keyprovider.KeyMeta, error) {
if c.KeyName == "" {
return nil, nil, &keyprovider.ErrInvalidConfiguration{
Message: "no key name found",
}
}
if c.KeyLength == 0 {
c.KeyLength = defaultDataKeyLength
}
if err := c.KeyLength.Validate(); err != nil {
return nil, nil, &keyprovider.ErrInvalidConfiguration{
Cause: err,
}
}
if c.TransitEnginePath == "" {
c.TransitEnginePath = defaultTransitEnginePath
}
// DefaultConfig reads BAO_ADDR and some other optional env variables.
config := openbao.DefaultConfig()
if config.Error != nil {
return nil, nil, &keyprovider.ErrInvalidConfiguration{
Cause: config.Error,
}
}
// Address from HCL supersedes BAO_ADDR.
if c.Address != "" {
config.Address = c.Address
}
client, err := newClient(config, c.Token)
if err != nil {
return nil, nil, &keyprovider.ErrInvalidConfiguration{
Cause: err,
}
}
return &keyProvider{
svc: service{
c: client,
transitPath: c.TransitEnginePath,
},
keyName: c.KeyName,
keyLength: c.KeyLength,
}, new(keyMeta), nil
}
type DataKeyLength int
func (l DataKeyLength) Validate() error {
switch l {
case 16, 32, 64:
return nil
default:
return fmt.Errorf("data key length should one of 16, 32 or 64 bytes: got %v", l)
}
}
func (l DataKeyLength) Bits() int {
return int(l) * 8
}
type clientConstructor func(config *openbao.Config, token string) (client, error)
// newClient variable allows to inject different client implementations.
// In order to keep client interface simple, token setting is in this function as well.
// It's not possible to pass token in config.
var newClient clientConstructor = newOpenBaoClient
func newOpenBaoClient(config *openbao.Config, token string) (client, error) {
// NewClient reads BAO_TOKEN and some other optional env variables.
c, err := openbao.NewClient(config)
if err != nil {
return nil, fmt.Errorf("error creating OpenBao client: %w", err)
}
// Token from HCL supersedes BAO_TOKEN.
if token != "" {
c.SetToken(token)
}
return c.Logical(), nil
}

View File

@ -0,0 +1,18 @@
package openbao
import "github.com/opentofu/opentofu/internal/encryption/keyprovider"
func New() keyprovider.Descriptor {
return &descriptor{}
}
type descriptor struct {
}
func (f descriptor) ID() keyprovider.ID {
return "openbao"
}
func (f descriptor) ConfigStruct() keyprovider.Config {
return &Config{}
}

View File

@ -0,0 +1,23 @@
package openbao
import (
"context"
openbao "github.com/openbao/openbao/api"
)
type mockClientFunc func(ctx context.Context, path string, data map[string]interface{}) (*openbao.Secret, error)
func (f mockClientFunc) WriteWithContext(ctx context.Context, path string, data map[string]interface{}) (*openbao.Secret, error) {
return f(ctx, path, data)
}
func injectMock(m mockClientFunc) {
newClient = func(_ *openbao.Config, _ string) (client, error) {
return m, nil
}
}
func injectDefaultClient() {
newClient = newOpenBaoClient
}

View File

@ -0,0 +1,66 @@
package openbao
import (
"context"
"github.com/opentofu/opentofu/internal/encryption/keyprovider"
)
type keyMeta struct {
Ciphertext []byte `json:"ciphertext"`
}
func (m keyMeta) isPresent() bool {
return len(m.Ciphertext) != 0
}
type keyProvider struct {
svc service
keyName string
keyLength DataKeyLength
}
func (p keyProvider) Provide(rawMeta keyprovider.KeyMeta) (keyprovider.Output, keyprovider.KeyMeta, error) {
if rawMeta == nil {
return keyprovider.Output{}, nil, &keyprovider.ErrInvalidMetadata{
Message: "bug: no metadata struct provided",
}
}
inMeta, ok := rawMeta.(*keyMeta)
if !ok {
return keyprovider.Output{}, nil, &keyprovider.ErrInvalidMetadata{
Message: "bug: invalid metadata struct type",
}
}
ctx := context.Background()
dataKey, err := p.svc.generateDataKey(ctx, p.keyName, p.keyLength.Bits())
if err != nil {
return keyprovider.Output{}, nil, &keyprovider.ErrKeyProviderFailure{
Message: "failed to generate OpenBao data key (check if the configuration valid and OpenBao server accessible)",
Cause: err,
}
}
outMeta := &keyMeta{
Ciphertext: dataKey.Ciphertext,
}
out := keyprovider.Output{
EncryptionKey: dataKey.Plaintext,
}
if inMeta.isPresent() {
out.DecryptionKey, err = p.svc.decryptData(ctx, p.keyName, inMeta.Ciphertext)
if err != nil {
return keyprovider.Output{}, nil, &keyprovider.ErrKeyProviderFailure{
Message: "failed to decrypt ciphertext (check if the configuration valid and OpenBao server accessible)",
Cause: err,
}
}
}
return out, outMeta, nil
}

View File

@ -14,6 +14,7 @@ import AESGCM from '!!raw-loader!./examples/encryption/aes_gcm.tf'
import PBKDF2 from '!!raw-loader!./examples/encryption/pbkdf2.tf'
import AWSKMS from '!!raw-loader!./examples/encryption/aws_kms.tf'
import GCPKMS from '!!raw-loader!./examples/encryption/gcp_kms.tf'
import OpenBao from '!!raw-loader!./examples/encryption/openbao.tf'
import Fallback from '!!raw-loader!./examples/encryption/fallback.tf'
import FallbackFromUnencrypted from '!!raw-loader!./examples/encryption/fallback_from_unencrypted.tf'
import FallbackToUnencrypted from '!!raw-loader!./examples/encryption/fallback_to_unencrypted.tf'
@ -126,6 +127,22 @@ The following example illustrates a minimal configuration:
<CodeBlock language="hcl">{GCPKMS}</CodeBlock>
### OpenBao
This key provider uses the [OpenBao Transit Secret Engine](https://janma.github.io/openbao/docs/secrets/transit) to generate data keys. You can configure it as follows:
| Option | Description | Min. | Default |
|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------|------------------------|
| key_name *(required)* | Name of the transit encryption key to use to encrypt/decrypt the datakey. It should be [pre-configured](https://janma.github.io/openbao/docs/secrets/transit/#setup) in OpenBao server. | N/A | - |
| token | [Authorization Token](https://janma.github.io/openbao/docs/concepts/tokens/) to use when accessing OpenBao API. Available as `BAO_TOKEN` environment variable. | N/A | - |
| address | OpenBao server address to access the API. Available as `BAO_ADDR` environment variable. | N/A | https://127.0.0.1:8200 |
| transit_engine_path | Path at whick Transit Secret Engine enabled in OpenBao. | N/A | /transit |
| key_length | Number of bytes to generate as a key. Available options are `16`, `32` or `64` bytes. | 16 | 32 |
The following example illustrates possible configuration:
<CodeBlock language="hcl">{OpenBao}</CodeBlock>
## Methods
### AES-GCM

View File

@ -0,0 +1,23 @@
terraform {
encryption {
key_provider "openbao" "my_bao" {
# Required. Name of the transit encryption key to use to encrypt/decrypt the datakey.
key_name = "test-key"
# Optional. Authorization Token to use when accessing OpenBao API.
# Could be set via BAO_TOKEN environment variable.
token = "s.Fg8wA4nDrP08TirpjEXkrTmt"
# Optional. OpenBao server address to access the API.
# Could be set via BAO_ADDR environment variable.
address = "http://127.0.0.1:8200"
# Optional. Path at whick Transit Secret Engine enabled in OpenBao.
transit_engine_path = "/my-org/transit"
# Optional. Number of bytes to generate as a key.
key_length = 16
}
}
}