├── .dockerignore ├── .github └── workflows │ ├── docker.yaml │ ├── semgrep.yml │ └── tests.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.org ├── cmd ├── lockbox-controller │ ├── Dockerfile │ ├── keypair.go │ └── main.go ├── lockbox-keypair │ └── main.go └── locket │ └── main.go ├── deployment ├── crds │ └── lockbox.k8s.cloudflare.com_lockboxes.yaml ├── manifests │ ├── deployment-lockbox.yaml │ ├── namespace-lockbox.yaml │ ├── service-lockbox.yaml │ └── serviceaccount-lockbox.yaml └── rbac │ ├── proxier.yaml │ ├── role-binding.yaml │ └── role.yaml ├── go.mod ├── go.sum ├── pkg ├── apis │ └── lockbox.k8s.cloudflare.com │ │ └── v1 │ │ ├── groupversion_info.go │ │ ├── lockbox.go │ │ ├── lockbox_test.go │ │ ├── types.go │ │ └── zz_generated.deepcopy.go ├── flagvar │ ├── enum.go │ ├── enum_test.go │ ├── file.go │ ├── file_test.go │ ├── tcp_addr.go │ ├── tcp_addr_test.go │ └── testdata │ │ └── file ├── lockbox-controller │ ├── secretreconciler.go │ ├── secretreconciler_suite_test.go │ └── secretreconciler_test.go ├── lockbox-server │ └── serve.go ├── statemetrics │ ├── collector.go │ ├── handler.go │ ├── handler_test.go │ ├── labels.go │ └── labels_test.go └── util │ └── conditions │ └── conditions.go └── tools └── tools.go /.dockerignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | on: 3 | - pull_request 4 | - push 5 | jobs: 6 | docker: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: docker/setup-qemu-action@v3 11 | - uses: docker/metadata-action@v5 12 | id: docker-meta 13 | with: 14 | images: cloudflare/lockbox 15 | - uses: docker/setup-buildx-action@v3 16 | - uses: docker/login-action@v3 17 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 18 | with: 19 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 20 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 21 | - uses: docker/build-push-action@v5 22 | with: 23 | file: ./cmd/lockbox-controller/Dockerfile 24 | platforms: linux/amd64, linux/arm64 25 | tags: ${{ steps.docker-meta.outputs.tags }} 26 | push: ${{ startsWith(github.ref, 'refs/tags/v') }} 27 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: {} 3 | workflow_dispatch: {} 4 | push: 5 | branches: 6 | - trunk 7 | schedule: 8 | - cron: "0 0 * * *" 9 | name: Semgrep config 10 | jobs: 11 | semgrep: 12 | name: semgrep/ci 13 | runs-on: ubuntu-latest 14 | env: 15 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 16 | SEMGREP_URL: https://cloudflare.semgrep.dev 17 | SEMGREP_APP_URL: https://cloudflare.semgrep.dev 18 | SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version 19 | container: 20 | image: semgrep/semgrep 21 | steps: 22 | - uses: actions/checkout@v4 23 | - run: semgrep ci 24 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | - pull_request 4 | - push 5 | jobs: 6 | unit: 7 | runs-on: ubuntu-latest 8 | name: "Go ${{ matrix.go }} Test" 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-go@v4 12 | with: 13 | go-version: "stable" 14 | - run: make test 15 | lint: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-go@v4 20 | with: 21 | go-version: "stable" 22 | - uses: dominikh/staticcheck-action@v1 23 | with: 24 | build-tags: suite 25 | install-go: false 26 | integration: 27 | needs: 28 | - unit 29 | - lint 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: actions/setup-go@v4 34 | with: 35 | go-version: "stable" 36 | - run: | 37 | go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest 38 | source <(setup-envtest use -p env) 39 | go test ./... -tags suite 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Go.gitignore ## 2 | 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Vendor directory 14 | /vendor 15 | 16 | ## nix.gitignore ## 17 | 18 | /result* 19 | /bin/ 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Cloudflare, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := binaries 2 | 3 | KERNEL := $(shell uname -s) 4 | GOTESTSUM := $(shell command -v gotestsum 2> /dev/null) 5 | 6 | DIB ?= docker 7 | IMAGE_ROOT ?= localhost/lockbox 8 | IMAGE_VERSION ?= $(shell git log -1 --pretty=format:%cd-%h --date short HEAD) 9 | VERSION := $(shell git describe --tags --always --dirty=-dev) 10 | # Build docker images for the native arch, but allow overriding in the environment for local development 11 | PLATFORM ?= local 12 | 13 | # Bind mount $SSL_CERT_FILE (or default) to build container if the file exists. 14 | SSL_CERT_FILE ?= /etc/ssl/certs/ca-certificates.crt 15 | ifneq (,$(wildcard ${SSL_CERT_FILE})) 16 | SECRETS = --secret id=certificates,src=${SSL_CERT_FILE} 17 | endif 18 | 19 | # When compiling for Linux enable Security's recommend hardening to satisfy `checksec' checks. 20 | # Unfortunately, most of these flags aren't portable to other operating systems. 21 | ifeq (${KERNEL},Linux) 22 | CGO_ENABLED ?= 1 23 | CPPFLAGS ?= -D_FORTIFY_SOURCE=2 -fstack-protector-all 24 | CFLAGS ?= -O2 -pipe -fno-plt 25 | CXXFLAGS ?= -O2 -pipe -fno-plt 26 | LDFLAGS ?= -Wl,-O1,-sort-common,-as-needed,-z,relro,-z,now 27 | GO_LDFLAGS ?= -linkmode=external 28 | GOFLAGS ?= -buildmode=pie 29 | endif 30 | 31 | GO_LDFLAGS += -w -s -X main.version=${VERSION} 32 | GOFLAGS += -v 33 | 34 | export CGO_ENABLED 35 | export CGO_CPPFLAGS ?= ${CPPFLAGS} 36 | export CGO_CFLAGS ?= ${CFLAGS} 37 | export CGO_CXXFLAGS ?= ${CXXFLAGS} 38 | export CGO_LDFLAGS ?= ${LDFLAGS} 39 | 40 | CMDS := $(shell find cmd -mindepth 1 -maxdepth 1 -type d | awk -F '/' '{ print $$NF }' ) 41 | IMAGES := $(shell find cmd -mindepth 1 -type f -name Dockerfile | awk -F '/' '{ print $$2 }') 42 | 43 | define make-go-target 44 | .PHONY: bin/$1 45 | bin/$1: 46 | go build ${GOFLAGS} -o $$@ -ldflags "${GO_LDFLAGS}" ./cmd/$1 47 | endef 48 | 49 | define make-dib-targets 50 | .PHONY: images/$1 51 | images/$1: 52 | ${DIB} buildx build --platform "$(PLATFORM)" ${SECRETS} -f cmd/$1/Dockerfile -t "${IMAGE_ROOT}/$1:${IMAGE_VERSION}" . 53 | 54 | .PHONY: push/images/$1 55 | push/images/$1: 56 | ${DIB} push "${IMAGE_ROOT}/$1:${IMAGE_VERSION}" 57 | endef 58 | 59 | $(foreach element,$(CMDS), $(eval $(call make-go-target,$(element)))) 60 | $(foreach element,$(IMAGES), $(eval $(call make-dib-targets,$(element)))) 61 | 62 | .PHONY: binaries 63 | binaries: $(CMDS:%=bin/%) 64 | 65 | .PHONY: images 66 | images: $(IMAGES:%=images/%) 67 | 68 | .PHONY: push-images 69 | push-images: $(IMAGES:%=push/images/%) 70 | 71 | .PHONY: clean 72 | clean: 73 | rm -rf bin 74 | 75 | .PHONY: test 76 | test: 77 | ifdef GOTESTSUM 78 | "${GOTESTSUM}" -- -count 1 ./... 79 | else 80 | go test -cover -count 1 ./... 81 | endif 82 | 83 | .PHONY: lint 84 | lint: 85 | staticcheck -tags suite ./... 86 | 87 | .PHONY: controller-gen 88 | controller-gen: 89 | go install sigs.k8s.io/controller-tools/cmd/controller-gen 90 | 91 | .PHONY: go-generate 92 | go-generate: controller-gen 93 | go generate -v ./... 94 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Lockbox 2 | 3 | [[https://pkg.go.dev/github.com/cloudflare/lockbox][https://pkg.go.dev/badge/github.com/cloudflare/lockbox.png]] 4 | 5 | Lockbox is a secure way to store Kubernetes Secrets offline. Secrets are asymmetrically encrypted, and can only be decrypted by the Lockbox Kubernetes controller. A companion CLI tool, =locket=, makes encrypting secrets a one-step process. 6 | 7 | ** Features 8 | + Secure encryption using modern cryptography. Uses Salsa20, Poly1305, and Curve25519. 9 | + Secrets are locked to specific namespaces. 10 | + All Kubernetes Secret types are supported. 11 | + Plays nicely with Secrets created by other controllers. 12 | + Continuously reconciles child resources. 13 | 14 | ** Example Usage 15 | Create a native Secret, but pass =--dry-run= to avoid submitting to the API. 16 | 17 | #+begin_example 18 | $ kubectl create secret generic mysecret --namespace default \ 19 | --from-literal=foo=bar --dry-run -o yaml > mysecret.yaml 20 | #+end_example 21 | 22 | Then, use locket to encrypt the secret. 23 | 24 | #+begin_example 25 | $ locket -f mysecret.yaml > mylockbox.yaml 26 | #+end_example 27 | 28 | Submit the lockbox to the API. 29 | 30 | #+begin_example 31 | $ kubectl create -f mylockbox.yaml 32 | #+end_example 33 | 34 | Remove the unencrypted secret. 35 | 36 | #+begin_example 37 | $ rm mysecret.yaml 38 | #+end_example 39 | -------------------------------------------------------------------------------- /cmd/lockbox-controller/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/library/golang:1.21.5-bookworm AS builder 2 | WORKDIR /go/src/app 3 | ADD . /go/src/app 4 | 5 | RUN --mount=type=cache,target=/go/pkg/mod \ 6 | --mount=type=cache,target=/root/.cache/go-build \ 7 | --mount=type=secret,id=certificates,target=/etc/ssl/certs/ca-certificates.crt \ 8 | make bin/lockbox-controller 9 | 10 | 11 | FROM gcr.io/distroless/base-nossl-debian12:nonroot 12 | COPY --from=builder /go/src/app/bin/lockbox-controller /bin 13 | ENTRYPOINT ["/bin/lockbox-controller"] 14 | -------------------------------------------------------------------------------- /cmd/lockbox-controller/keypair.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/kevinburke/nacl" 8 | "sigs.k8s.io/yaml" 9 | ) 10 | 11 | type kp struct { 12 | Private []byte `json:"private"` 13 | Public []byte `json:"public"` 14 | } 15 | 16 | // KeyPairFromYAMLOrJSON loads a public/private NaCL keypair from a YAML or JSON file. 17 | func KeyPairFromYAMLOrJSON(r io.Reader) (pub, pri nacl.Key, err error) { 18 | data, err := io.ReadAll(r) 19 | if err != nil { 20 | return 21 | } 22 | 23 | keypair := kp{} 24 | err = yaml.Unmarshal(data, &keypair, yaml.DisallowUnknownFields) 25 | if err != nil { 26 | return 27 | } 28 | 29 | if len(keypair.Private) != 32 { 30 | err = fmt.Errorf("incorrect private key length: %d, should be 32", len(keypair.Private)) 31 | return 32 | } 33 | if len(keypair.Public) != 32 { 34 | err = fmt.Errorf("incorrect public key length: %d, should be 32", len(keypair.Public)) 35 | return 36 | } 37 | 38 | pub = new([nacl.KeySize]byte) 39 | pri = new([nacl.KeySize]byte) 40 | 41 | copy(pri[:], keypair.Private) 42 | copy(pub[:], keypair.Public) 43 | return 44 | } 45 | -------------------------------------------------------------------------------- /cmd/lockbox-controller/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "os" 10 | "runtime" 11 | "time" 12 | 13 | lockboxv1 "github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare.com/v1" 14 | "github.com/cloudflare/lockbox/pkg/flagvar" 15 | lockboxcontroller "github.com/cloudflare/lockbox/pkg/lockbox-controller" 16 | server "github.com/cloudflare/lockbox/pkg/lockbox-server" 17 | "github.com/cloudflare/lockbox/pkg/statemetrics" 18 | "github.com/go-logr/zerologr" 19 | "github.com/kevinburke/nacl" 20 | "github.com/rs/zerolog" 21 | corev1 "k8s.io/api/core/v1" 22 | "k8s.io/client-go/kubernetes/scheme" 23 | "sigs.k8s.io/controller-runtime/pkg/cache" 24 | "sigs.k8s.io/controller-runtime/pkg/client/config" 25 | "sigs.k8s.io/controller-runtime/pkg/controller" 26 | "sigs.k8s.io/controller-runtime/pkg/handler" 27 | logf "sigs.k8s.io/controller-runtime/pkg/log" 28 | "sigs.k8s.io/controller-runtime/pkg/manager" 29 | "sigs.k8s.io/controller-runtime/pkg/manager/signals" 30 | "sigs.k8s.io/controller-runtime/pkg/metrics" 31 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 32 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 33 | "sigs.k8s.io/controller-runtime/pkg/source" 34 | ) 35 | 36 | var ( 37 | pubKey, priKey nacl.Key 38 | version = "dev" 39 | syncPeriod = 1 * time.Hour 40 | keypairPath = flagvar.File{Value: "/etc/lockbox/keypair.yaml"} 41 | metricsAddr = flagvar.TCPAddr{Text: ":8080"} 42 | httpAddr = flagvar.TCPAddr{Text: ":8081"} 43 | ) 44 | 45 | func main() { 46 | flag.Var(&keypairPath, "keypair", fmt.Sprintf("public/private 32 byte keypairs (%s)", keypairPath.Help())) 47 | flag.Var(&metricsAddr, "metrics-addr", fmt.Sprintf("bind for HTTP metrics (%s)", metricsAddr.Help())) 48 | flag.Var(&httpAddr, "http-addr", fmt.Sprintf("bind for HTTP server (%s)", httpAddr.Help())) 49 | flag.DurationVar(&syncPeriod, "sync-period", syncPeriod, "controller sync period") 50 | flag.String("v", "", "log level for V logs") 51 | flag.Parse() 52 | 53 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs 54 | zerologr.NameFieldName = "logger" 55 | zerologr.NameSeparator = "/" 56 | 57 | zl := zerolog.New(os.Stderr).With().Caller().Timestamp().Logger() 58 | logf.SetLogger(zerologr.New(&zl)) 59 | logger := zl.With().Str("name", "main").Logger() 60 | 61 | keypair, err := os.Open(keypairPath.Value) 62 | if err != nil { 63 | logger.Fatal().Err(err).Str("path", keypairPath.Value).Msg("unable to open keypair") 64 | os.Exit(1) 65 | } 66 | pubKey, priKey, err = KeyPairFromYAMLOrJSON(keypair) 67 | if err != nil { 68 | logger.Fatal().Err(err).Str("path", keypairPath.Value).Msg("unable to parse keypair") 69 | os.Exit(1) 70 | } 71 | keypair.Close() 72 | 73 | err = lockboxv1.AddToScheme(scheme.Scheme) 74 | if err != nil { 75 | logger.Fatal().Err(err).Msg("unable to add lockbox schemes") 76 | os.Exit(1) 77 | } 78 | 79 | cfg, err := config.GetConfig() 80 | cfg.UserAgent = fmt.Sprintf("%s/%s (%s/%s)", os.Args[0], version, runtime.GOOS, runtime.GOARCH) 81 | 82 | if err != nil { 83 | logger.Fatal().Err(err).Msg("unable to get kubeconfig") 84 | os.Exit(1) 85 | } 86 | 87 | mgr, err := manager.New(cfg, manager.Options{ 88 | Metrics: metricsserver.Options{ 89 | BindAddress: metricsAddr.Text, 90 | }, 91 | Cache: cache.Options{ 92 | SyncPeriod: &syncPeriod, 93 | }, 94 | Scheme: scheme.Scheme, 95 | }) 96 | if err != nil { 97 | logger.Fatal().Err(err).Msg("unable to create controller manager") 98 | os.Exit(1) 99 | } 100 | 101 | recorder := mgr.GetEventRecorderFor("lockbox") 102 | client := mgr.GetClient() 103 | 104 | sr := lockboxcontroller.NewSecretReconciler(pubKey, priKey, lockboxcontroller.WithRecorder(recorder), lockboxcontroller.WithClient(client)) 105 | 106 | info := statemetrics.NewKubernetesVec(statemetrics.KubernetesOpts{ 107 | Name: "kube_lockbox_info", 108 | Help: "Information about Lockbox", 109 | }, []string{"namespace", "lockbox"}) 110 | created := statemetrics.NewKubernetesVec(statemetrics.KubernetesOpts{ 111 | Name: "kube_lockbox_created", 112 | Help: "Unix creation timestamp", 113 | }, []string{"namespace", "lockbox"}) 114 | resourceVersion := statemetrics.NewKubernetesVec(statemetrics.KubernetesOpts{ 115 | Name: "kube_lockbox_resource_version", 116 | Help: "Resource version representing a specific version of a Lockbox", 117 | }, []string{"namespace", "lockbox", "resource_version"}) 118 | lbType := statemetrics.NewKubernetesVec(statemetrics.KubernetesOpts{ 119 | Name: "kube_lockbox_type", 120 | Help: "Lockbox secret type", 121 | }, []string{"namespace", "lockbox", "type"}) 122 | peerKey := statemetrics.NewKubernetesVec(statemetrics.KubernetesOpts{ 123 | Name: "kube_lockbox_peer", 124 | Help: "Lockbox peer key", 125 | }, []string{"namespace", "lockbox", "peer"}) 126 | labels := statemetrics.NewLabelsVec(statemetrics.KubernetesOpts{ 127 | Name: "kube_lockbox_labels", 128 | Help: "Kubernetes labels converted to Prometheus labels", 129 | }) 130 | metrics.Registry.MustRegister(info, created, resourceVersion, lbType, labels, peerKey) 131 | 132 | mh := statemetrics.NewStateMetricProxy( 133 | &handler.EnqueueRequestForObject{}, 134 | info, created, resourceVersion, 135 | lbType, peerKey, labels, 136 | ) 137 | 138 | c, err := controller.New("lockbox-controller", mgr, controller.Options{ 139 | Reconciler: reconcile.AsReconciler(mgr.GetClient(), sr), 140 | }) 141 | 142 | if err != nil { 143 | logger.Fatal().Err(err).Msg("unable to create controller") 144 | os.Exit(1) 145 | } 146 | 147 | if err := c.Watch(source.Kind(mgr.GetCache(), &lockboxv1.Lockbox{}), mh); err != nil { 148 | logger.Fatal().Err(err).Msg("unable to watch Lockbox resources") 149 | os.Exit(1) 150 | } 151 | 152 | if err := c.Watch(source.Kind(mgr.GetCache(), &corev1.Secret{}), handler.EnqueueRequestForOwner(scheme.Scheme, mgr.GetRESTMapper(), &lockboxv1.Lockbox{}, handler.OnlyControllerOwner())); err != nil { 153 | logger.Fatal().Err(err).Msg("unable to watch Secret resources") 154 | os.Exit(1) 155 | } 156 | 157 | // TODO(terin): make server implement Runnable 158 | if err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error { 159 | mux := http.NewServeMux() 160 | mux.Handle("/v1/public", server.PublicKey(pubKey)) 161 | 162 | ln, err := net.Listen("tcp", httpAddr.Text) 163 | if err != nil { 164 | return err 165 | } 166 | 167 | // sig.kubernetes.io/controller-runtime/pkg/internal/httpserver 168 | s := http.Server{ 169 | Handler: mux, 170 | MaxHeaderBytes: 1 << 20, 171 | IdleTimeout: 90 * time.Second, 172 | ReadHeaderTimeout: 32 * time.Second, 173 | } 174 | 175 | idleConnsClosed := make(chan struct{}) 176 | go func() { 177 | <-ctx.Done() 178 | 179 | if err := s.Shutdown(context.Background()); err != nil { 180 | logger.Err(err).Send() 181 | } 182 | close(idleConnsClosed) 183 | }() 184 | 185 | if err := s.Serve(ln); err != nil && err != http.ErrServerClosed { 186 | return err 187 | } 188 | 189 | <-idleConnsClosed 190 | return nil 191 | })); err != nil { 192 | logger.Fatal().Err(err).Msg("unable to add server runnable") 193 | } 194 | 195 | if err := mgr.Start(signals.SetupSignalHandler()); err != nil { 196 | logger.Fatal().Err(err).Send() 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /cmd/lockbox-keypair/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/kevinburke/nacl/box" 10 | ) 11 | 12 | func main() { 13 | lockboxPubKey, lockboxPriKey, err := box.GenerateKey(rand.Reader) 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | pub64 := base64.StdEncoding.EncodeToString(lockboxPubKey[:]) 19 | pri64 := base64.StdEncoding.EncodeToString(lockboxPriKey[:]) 20 | 21 | fmt.Fprintf(os.Stdout, "public: %s\nprivate: %s\n", pub64, pri64) 22 | } 23 | -------------------------------------------------------------------------------- /cmd/locket/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "os" 10 | gruntime "runtime" 11 | "time" 12 | 13 | lockboxv1 "github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare.com/v1" 14 | "github.com/cloudflare/lockbox/pkg/flagvar" 15 | "github.com/go-logr/zerologr" 16 | "github.com/kevinburke/nacl" 17 | "github.com/kevinburke/nacl/box" 18 | "github.com/rs/zerolog" 19 | corev1 "k8s.io/api/core/v1" 20 | "k8s.io/apimachinery/pkg/runtime" 21 | runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" 22 | "k8s.io/client-go/kubernetes" 23 | "k8s.io/client-go/kubernetes/scheme" 24 | "k8s.io/client-go/tools/clientcmd" 25 | clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 26 | logf "sigs.k8s.io/controller-runtime/pkg/log" 27 | ) 28 | 29 | var ( 30 | input = flagvar.File{} 31 | kubeconfig = flagvar.File{} 32 | output = flagvar.Enum{Choices: []string{"json", "yaml"}, Value: "yaml"} 33 | version = "dev" 34 | printVersion bool 35 | peerHex string 36 | masterURL string 37 | lockboxNS string 38 | lockboxSvc string 39 | ) 40 | 41 | func main() { 42 | flag.Var(&input, "f", fmt.Sprintf("input file (%s)", input.Help())) 43 | flag.Var(&output, "o", fmt.Sprintf("output format (%s)", output.Help())) 44 | flag.Var(&kubeconfig, "kubeconfig", fmt.Sprintf("path to kubeconfig. (%s)", kubeconfig.Help())) 45 | flag.StringVar(&peerHex, "peer-hex", "", "peer public key (32-bit hex)") 46 | flag.StringVar(&masterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") 47 | flag.StringVar(&lockboxNS, "lockbox-namespace", "lockbox", "namespace of the lockbox controller") 48 | flag.StringVar(&lockboxSvc, "lockbox-service", "lockbox", "name of the lockbox service") 49 | flag.BoolVar(&printVersion, "version", false, "print version") 50 | flag.String("v", "", "log level for V logs") 51 | flag.Parse() 52 | 53 | ctx := context.Background() 54 | 55 | if printVersion { 56 | fmt.Printf("locket: %s\n", version) 57 | os.Exit(0) 58 | } 59 | 60 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs 61 | zerologr.NameFieldName = "logger" 62 | zerologr.NameSeparator = "/" 63 | 64 | zl := zerolog.New(os.Stderr).With().Caller().Timestamp().Logger() 65 | logf.SetLogger(zerologr.New(&zl)) 66 | logger := zl.With().Str("name", "main").Logger() 67 | 68 | err := lockboxv1.AddToScheme(scheme.Scheme) 69 | if err != nil { 70 | logger.Fatal().Err(err).Msg("unable to add lockbox schemes") 71 | os.Exit(1) 72 | } 73 | 74 | var r io.Reader 75 | if input.String() == "" { 76 | r = os.Stdin 77 | } else { 78 | r, err = os.Open(input.String()) 79 | if err != nil { 80 | logger.Fatal().Err(err).Msg("unable to open secret file") 81 | os.Exit(1) 82 | } 83 | } 84 | 85 | w := os.Stdout 86 | 87 | cfg := GetConfig() 88 | 89 | cf := runtimeserializer.NewCodecFactory(scheme.Scheme) 90 | 91 | ib, err := io.ReadAll(r) 92 | if err != nil { 93 | logger.Fatal().Err(err).Msg("unable to read secret file") 94 | os.Exit(1) 95 | } 96 | var secret corev1.Secret 97 | if err = runtime.DecodeInto(cf.UniversalDecoder(), ib, &secret); err != nil { 98 | logger.Fatal().Err(err).Msg("unable to decode secret file") 99 | os.Exit(1) 100 | } 101 | 102 | pubKey, priKey, err := box.GenerateKey(rand.Reader) 103 | if err != nil { 104 | logger.Fatal().Err(err).Msg("could not generate key") 105 | os.Exit(1) 106 | } 107 | 108 | var peerKey nacl.Key 109 | switch peerHex { 110 | case "": 111 | ctx, cancel := context.WithTimeout(ctx, 30*time.Second) 112 | defer cancel() 113 | 114 | cc, err := cfg.ClientConfig() 115 | if err != nil { 116 | logger.Fatal().Err(err).Msg("unable to create API client configuration") 117 | os.Exit(1) 118 | } 119 | 120 | cc.UserAgent = fmt.Sprintf("%s/%s (%s/%s)", os.Args[0], version, gruntime.GOOS, gruntime.GOARCH) 121 | 122 | client, err := kubernetes.NewForConfig(cc) 123 | if err != nil { 124 | logger.Fatal().Err(err).Msg("unable to create API client") 125 | os.Exit(1) 126 | } 127 | 128 | b, err := GetRemotePublicKey(ctx, client, lockboxNS, lockboxSvc) 129 | if err != nil { 130 | logger.Fatal().Err(err).Msg("unable to fetch public key") 131 | os.Exit(1) 132 | } 133 | if len(b) != 32 { 134 | err = fmt.Errorf("incorrect peer key length: %d, should be 32", len(b)) 135 | logger.Fatal().Err(err).Msg("unable to fetch peer key") 136 | return 137 | } 138 | 139 | peerKey = new([nacl.KeySize]byte) 140 | copy(peerKey[:], b) 141 | default: 142 | peerKey, err = nacl.Load(peerHex) 143 | if err != nil { 144 | logger.Fatal().Err(err).Msg("could not load --peer-hex") 145 | os.Exit(1) 146 | } 147 | } 148 | 149 | namespace := secret.Namespace 150 | if namespace == "" { 151 | namespace, _, _ = cfg.Namespace() 152 | } 153 | 154 | b := lockboxv1.NewFromSecret(secret, namespace, peerKey, pubKey, priKey) 155 | 156 | var ct string 157 | switch output.String() { 158 | case "yaml": 159 | ct = "application/yaml" 160 | case "json": 161 | ct = "application/json" 162 | } 163 | 164 | info, ok := runtime.SerializerInfoForMediaType(cf.SupportedMediaTypes(), ct) 165 | if !ok { 166 | logger.Fatal().Str("content-type", ct).Msg("can't serialize to content-type") 167 | os.Exit(1) 168 | } 169 | serial := info.Serializer 170 | if info.PrettySerializer != nil { 171 | serial = info.PrettySerializer 172 | } 173 | enc := cf.EncoderForVersion(serial, lockboxv1.GroupVersion) 174 | 175 | ob, err := runtime.Encode(enc, b) 176 | if err != nil { 177 | logger.Fatal().Err(err).Msg("unable to encode Lockbox") 178 | os.Exit(1) 179 | } 180 | 181 | if _, err := w.Write(ob); err != nil { 182 | logger.Fatal().Err(err).Send() 183 | } 184 | if _, err := w.WriteString("\n"); err != nil { 185 | logger.Fatal().Err(err).Send() 186 | } 187 | } 188 | 189 | func GetConfig() clientcmd.ClientConfig { 190 | loader := clientcmd.NewDefaultClientConfigLoadingRules() 191 | overrides := clientcmd.ConfigOverrides{ 192 | ClusterInfo: clientcmdapi.Cluster{ 193 | Server: masterURL, 194 | }, 195 | } 196 | loader.ExplicitPath = kubeconfig.String() 197 | return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, &overrides) 198 | } 199 | 200 | func GetRemotePublicKey(ctx context.Context, c kubernetes.Interface, ns, svc string) ([]byte, error) { 201 | return c.CoreV1().Services(ns).ProxyGet("http", svc, "", "/v1/public", nil).DoRaw(ctx) 202 | } 203 | -------------------------------------------------------------------------------- /deployment/crds/lockbox.k8s.cloudflare.com_lockboxes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.13.0 7 | name: lockboxes.lockbox.k8s.cloudflare.com 8 | spec: 9 | group: lockbox.k8s.cloudflare.com 10 | names: 11 | kind: Lockbox 12 | listKind: LockboxList 13 | plural: lockboxes 14 | singular: lockbox 15 | scope: Namespaced 16 | versions: 17 | - additionalPrinterColumns: 18 | - jsonPath: .spec.template.type 19 | name: SecretType 20 | type: string 21 | - jsonPath: .spec.peer 22 | name: Peer 23 | type: string 24 | name: v1 25 | schema: 26 | openAPIV3Schema: 27 | description: Lockbox is a struct wrapping the LockboxSpec in standard API 28 | server metadata fields. 29 | properties: 30 | apiVersion: 31 | description: 'APIVersion defines the versioned schema of this representation 32 | of an object. Servers should convert recognized schemas to the latest 33 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 34 | type: string 35 | kind: 36 | description: 'Kind is a string value representing the REST resource this 37 | object represents. Servers may infer this from the endpoint the client 38 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 39 | type: string 40 | metadata: 41 | type: object 42 | spec: 43 | description: Desired state of the Lockbox resource. 44 | properties: 45 | data: 46 | additionalProperties: 47 | format: byte 48 | type: string 49 | description: Data contains the secret data, encrypted to the Peer's 50 | public key. Each key in the data map must consist of alphanumeric 51 | characters, '-', '_', or '.'. 52 | type: object 53 | namespace: 54 | description: Namespace stores an encrypted copy of which namespace 55 | this Lockbox is locked for, ensuring it cannot be deployed to another 56 | namespace under an attacker's control. 57 | format: byte 58 | type: string 59 | peer: 60 | description: Peer stores the public key that can unlock this Lockbox. 61 | format: byte 62 | type: string 63 | sender: 64 | description: Sender stores the public key used to lock this Lockbox. 65 | format: byte 66 | type: string 67 | template: 68 | description: Template defines the structure of the Secret that will 69 | be created from this Lockbox. 70 | properties: 71 | metadata: 72 | properties: 73 | annotations: 74 | additionalProperties: 75 | type: string 76 | description: 'Annotations is an unstructured key value map 77 | stored with a resource that may be set by external tools 78 | to store and retrieve arbitrary metadata. They are not queryable 79 | and should be preserved when modifying objects. More info: 80 | http://kubernetes.io/docs/user-guide/annotations' 81 | type: object 82 | labels: 83 | additionalProperties: 84 | type: string 85 | description: 'Map of string keys and values that can be used 86 | to organize and categorize (scope and select) objects. May 87 | match selectors of replication controllers and services. 88 | More info: http://kubernetes.io/docs/user-guide/labels' 89 | type: object 90 | type: object 91 | type: 92 | description: Type is used to facilitate programmatic handling 93 | of secret data. 94 | type: string 95 | type: object 96 | required: 97 | - data 98 | - namespace 99 | - peer 100 | - sender 101 | type: object 102 | status: 103 | description: Status of the Lockbox. This is set and managed automatically. 104 | properties: 105 | conditions: 106 | description: List of status conditions to indicate the status of a 107 | Lockbox. 108 | items: 109 | description: Condition contains condition information for a Lockbox. 110 | properties: 111 | lastTransitionTime: 112 | description: LastTransitionTime marks when the condition last 113 | transitioned from one status to another. This should be when 114 | the underlying condition changed. If that is not known, then 115 | using the time when the API field changed is acceptable. 116 | format: date-time 117 | type: string 118 | message: 119 | description: A message is the human readable message indicating 120 | details about the transition. The field may be empty. 121 | type: string 122 | reason: 123 | description: The reason for the condition's last transition 124 | in CamelCase. 125 | type: string 126 | severity: 127 | description: Severity provides explicit classification of Reason 128 | code, so that users or machines can immediately understand 129 | the current situation and act accordingly. The Severity field 130 | MUST be set only when Status=False. 131 | enum: 132 | - Error 133 | - Warning 134 | - Info 135 | type: string 136 | status: 137 | description: Status of the condition, one of True, False, Unknown 138 | type: string 139 | type: 140 | description: Type of condition in CamelCase. 141 | enum: 142 | - Ready 143 | type: string 144 | required: 145 | - status 146 | - type 147 | type: object 148 | type: array 149 | type: object 150 | required: 151 | - spec 152 | type: object 153 | served: true 154 | storage: true 155 | subresources: 156 | status: {} 157 | -------------------------------------------------------------------------------- /deployment/manifests/deployment-lockbox.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: lockbox-controller 5 | namespace: lockbox 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: lockbox 11 | component: controller 12 | template: 13 | metadata: 14 | labels: 15 | app: lockbox 16 | component: controller 17 | spec: 18 | serviceAccountName: lockbox-controller 19 | containers: 20 | - name: lockbox 21 | image: cloudflare/lockbox:v0.6.0 22 | ports: 23 | - containerPort: 8080 24 | name: http-metrics 25 | - containerPort: 8081 26 | name: http-api 27 | volumeMounts: 28 | - name: keypair 29 | mountPath: /etc/lockbox/ 30 | readOnly: true 31 | volumes: 32 | - name: keypair 33 | secret: 34 | secretName: keypair 35 | defaultMode: 256 36 | -------------------------------------------------------------------------------- /deployment/manifests/namespace-lockbox.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: lockbox 5 | -------------------------------------------------------------------------------- /deployment/manifests/service-lockbox.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: lockbox 5 | namespace: lockbox 6 | spec: 7 | ports: 8 | - port: 80 9 | targetPort: 8081 10 | selector: 11 | app: lockbox 12 | component: controller 13 | -------------------------------------------------------------------------------- /deployment/manifests/serviceaccount-lockbox.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: lockbox-controller 5 | namespace: lockbox 6 | -------------------------------------------------------------------------------- /deployment/rbac/proxier.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: lockbox-proxier 5 | namespace: lockbox 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - "services/proxy" 11 | resourceNames: 12 | - "http:lockbox:" 13 | verbs: 14 | - "get" 15 | --- 16 | apiVersion: rbac.authorization.k8s.io/v1 17 | kind: RoleBinding 18 | metadata: 19 | name: lockbox-proxier 20 | namespace: lockbox 21 | roleRef: 22 | apiGroup: rbac.authorization.k8s.io 23 | kind: Role 24 | name: lockbox-proxier 25 | subjects: 26 | - kind: Group 27 | name: system:authenticated 28 | -------------------------------------------------------------------------------- /deployment/rbac/role-binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: lockbox-controller 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: lockbox-controller 9 | subjects: 10 | - kind: ServiceAccount 11 | name: lockbox-controller 12 | namespace: lockbox 13 | -------------------------------------------------------------------------------- /deployment/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: lockbox-controller 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - events 11 | verbs: 12 | - create 13 | - patch 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - secrets 18 | verbs: 19 | - create 20 | - get 21 | - list 22 | - patch 23 | - update 24 | - watch 25 | - apiGroups: 26 | - lockbox.k8s.cloudflare.com 27 | resources: 28 | - lockboxes 29 | verbs: 30 | - get 31 | - list 32 | - watch 33 | - apiGroups: 34 | - lockbox.k8s.cloudflare.com 35 | resources: 36 | - lockboxes/status 37 | verbs: 38 | - get 39 | - patch 40 | - update 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cloudflare/lockbox 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.5 6 | 7 | require ( 8 | github.com/go-logr/zerologr v1.2.3 9 | github.com/google/go-cmp v0.6.0 10 | github.com/kevinburke/nacl v0.0.0-20210405173606-cd9060f5f776 11 | github.com/prometheus/client_golang v1.18.0 12 | github.com/prometheus/common v0.45.0 13 | github.com/rs/zerolog v1.29.1 14 | gotest.tools/v3 v3.4.0 15 | k8s.io/api v0.29.0 16 | k8s.io/apimachinery v0.29.0 17 | k8s.io/client-go v0.29.0 18 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b 19 | sigs.k8s.io/controller-runtime v0.17.0 20 | sigs.k8s.io/controller-tools v0.13.0 21 | sigs.k8s.io/yaml v1.4.0 22 | ) 23 | 24 | require ( 25 | github.com/beorn7/perks v1.0.1 // indirect 26 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 27 | github.com/davecgh/go-spew v1.1.1 // indirect 28 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 29 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 30 | github.com/evanphx/json-patch/v5 v5.8.0 // indirect 31 | github.com/fatih/color v1.15.0 // indirect 32 | github.com/fsnotify/fsnotify v1.7.0 // indirect 33 | github.com/go-logr/logr v1.4.1 // indirect 34 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 35 | github.com/go-openapi/jsonreference v0.20.2 // indirect 36 | github.com/go-openapi/swag v0.22.3 // indirect 37 | github.com/gobuffalo/flect v1.0.2 // indirect 38 | github.com/gogo/protobuf v1.3.2 // indirect 39 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 40 | github.com/golang/protobuf v1.5.3 // indirect 41 | github.com/google/gnostic-models v0.6.8 // indirect 42 | github.com/google/gofuzz v1.2.0 // indirect 43 | github.com/google/uuid v1.3.0 // indirect 44 | github.com/imdario/mergo v0.3.12 // indirect 45 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 46 | github.com/josharian/intern v1.0.0 // indirect 47 | github.com/json-iterator/go v1.1.12 // indirect 48 | github.com/mailru/easyjson v0.7.7 // indirect 49 | github.com/mattn/go-colorable v0.1.13 // indirect 50 | github.com/mattn/go-isatty v0.0.17 // indirect 51 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 52 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 53 | github.com/modern-go/reflect2 v1.0.2 // indirect 54 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 55 | github.com/pkg/errors v0.9.1 // indirect 56 | github.com/prometheus/client_model v0.5.0 // indirect 57 | github.com/prometheus/procfs v0.12.0 // indirect 58 | github.com/spf13/cobra v1.7.0 // indirect 59 | github.com/spf13/pflag v1.0.5 // indirect 60 | golang.org/x/crypto v0.16.0 // indirect 61 | golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect 62 | golang.org/x/mod v0.14.0 // indirect 63 | golang.org/x/net v0.19.0 // indirect 64 | golang.org/x/oauth2 v0.12.0 // indirect 65 | golang.org/x/sys v0.16.0 // indirect 66 | golang.org/x/term v0.15.0 // indirect 67 | golang.org/x/text v0.14.0 // indirect 68 | golang.org/x/time v0.3.0 // indirect 69 | golang.org/x/tools v0.16.1 // indirect 70 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 71 | google.golang.org/appengine v1.6.7 // indirect 72 | google.golang.org/protobuf v1.31.0 // indirect 73 | gopkg.in/inf.v0 v0.9.1 // indirect 74 | gopkg.in/yaml.v2 v2.4.0 // indirect 75 | gopkg.in/yaml.v3 v3.0.1 // indirect 76 | k8s.io/apiextensions-apiserver v0.29.0 // indirect 77 | k8s.io/component-base v0.29.0 // indirect 78 | k8s.io/klog/v2 v2.110.1 // indirect 79 | k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect 80 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 81 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 82 | ) 83 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 4 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 6 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 7 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 12 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 13 | github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= 14 | github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 15 | github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro= 16 | github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= 17 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 18 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 19 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 20 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 21 | github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 22 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 23 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 24 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 25 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 26 | github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs= 27 | github.com/go-logr/zerologr v1.2.3/go.mod h1:BxwGo7y5zgSHYR1BjbnHPyF/5ZjVKfKxAZANVu6E8Ho= 28 | github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= 29 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 30 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 31 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 32 | github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= 33 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 34 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 35 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 36 | github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= 37 | github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= 38 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 39 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 40 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 41 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 42 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 43 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 44 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 45 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 46 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 47 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 48 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 49 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 50 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 51 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 52 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 53 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 54 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 55 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 56 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= 57 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 58 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 59 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 60 | github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= 61 | github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 62 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 63 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 64 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 65 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 66 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 67 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 68 | github.com/kevinburke/nacl v0.0.0-20210405173606-cd9060f5f776 h1:W8T7zJRO9imecUZySwPkuXHosjp2MloqAY1eSAEEOIo= 69 | github.com/kevinburke/nacl v0.0.0-20210405173606-cd9060f5f776/go.mod h1:VUp2yfq+wAk8hMl3NNN34fXjzUD9xMpGvUL8eSJz9Ns= 70 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 71 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 72 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 73 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 74 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 75 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 76 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 77 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 78 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 79 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 80 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 81 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 82 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 83 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 84 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 85 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 86 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 87 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 88 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= 89 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 90 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 91 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 92 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 93 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 94 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 95 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 96 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 97 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 98 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 99 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 100 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 101 | github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= 102 | github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= 103 | github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= 104 | github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= 105 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 106 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 107 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 108 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 109 | github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= 110 | github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= 111 | github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 112 | github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 113 | github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= 114 | github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= 115 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 116 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 117 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 118 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 119 | github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 120 | github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= 121 | github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= 122 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 123 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 124 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 125 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 126 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 127 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 128 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 129 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 130 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 131 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 132 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 133 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 134 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 135 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 136 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 137 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 138 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 139 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 140 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 141 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 142 | go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 143 | go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 144 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 145 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 146 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 147 | golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= 148 | golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 149 | golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= 150 | golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= 151 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 152 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 153 | golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= 154 | golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 155 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 156 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 157 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 158 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 159 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 160 | golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= 161 | golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= 162 | golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= 163 | golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= 164 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 165 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 166 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 167 | golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= 168 | golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 169 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 170 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 171 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 172 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 173 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 174 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 175 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 176 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 177 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 178 | golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= 179 | golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 180 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 181 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 182 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 183 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 184 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 185 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 186 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 187 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 188 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 189 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 190 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 191 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 192 | golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= 193 | golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= 194 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 195 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 196 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 197 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 198 | gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= 199 | gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 200 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 201 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 202 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 203 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 204 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 205 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 206 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 207 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 208 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 209 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 210 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 211 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 212 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 213 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 214 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 215 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 216 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 217 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 218 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 219 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 220 | gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= 221 | gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= 222 | k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= 223 | k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= 224 | k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3Q4lV0= 225 | k8s.io/apiextensions-apiserver v0.29.0/go.mod h1:TKmpy3bTS0mr9pylH0nOt/QzQRrW7/h7yLdRForMZwc= 226 | k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= 227 | k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= 228 | k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8= 229 | k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38= 230 | k8s.io/component-base v0.29.0 h1:T7rjd5wvLnPBV1vC4zWd/iWRbV8Mdxs+nGaoaFzGw3s= 231 | k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M= 232 | k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= 233 | k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= 234 | k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= 235 | k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= 236 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= 237 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 238 | sigs.k8s.io/controller-runtime v0.17.0 h1:fjJQf8Ukya+VjogLO6/bNX9HE6Y2xpsO5+fyS26ur/s= 239 | sigs.k8s.io/controller-runtime v0.17.0/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= 240 | sigs.k8s.io/controller-tools v0.13.0 h1:NfrvuZ4bxyolhDBt/rCZhDnx3M2hzlhgo5n3Iv2RykI= 241 | sigs.k8s.io/controller-tools v0.13.0/go.mod h1:5vw3En2NazbejQGCeWKRrE7q4P+CW8/klfVqP8QZkgA= 242 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 243 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 244 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= 245 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 246 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 247 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 248 | -------------------------------------------------------------------------------- /pkg/apis/lockbox.k8s.cloudflare.com/v1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // +kubebuilder:object:generate=true 2 | // +groupName=lockbox.k8s.cloudflare.com 3 | 4 | // Package v1 is the v1 version of the Lockbox API 5 | package v1 6 | 7 | import ( 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | "sigs.k8s.io/controller-runtime/pkg/scheme" 10 | ) 11 | 12 | //go:generate controller-gen object crd paths=./. output:crd:artifacts:config=../../../../deployment/crds 13 | 14 | var ( 15 | // GroupVersion is group version used to register these objects 16 | GroupVersion = schema.GroupVersion{Group: "lockbox.k8s.cloudflare.com", Version: "v1"} 17 | 18 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 19 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 20 | 21 | // AddToScheme adds the types in this group-version to the given scheme. 22 | AddToScheme = SchemeBuilder.AddToScheme 23 | ) 24 | 25 | func init() { 26 | SchemeBuilder.Register(&Lockbox{}, &LockboxList{}) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/apis/lockbox.k8s.cloudflare.com/v1/lockbox.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/kevinburke/nacl" 5 | "github.com/kevinburke/nacl/box" 6 | corev1 "k8s.io/api/core/v1" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | const keySize = nacl.KeySize 11 | 12 | // NewFromSecret creates a Lockbox wrapping the provided Secret. The value of each secret 13 | // are individually encrypted using the provided key pair. 14 | func NewFromSecret(secret corev1.Secret, namespace string, peer, pub, pri nacl.Key) *Lockbox { 15 | encNS := box.EasySeal([]byte(namespace), peer, pri) 16 | 17 | b := &Lockbox{ 18 | ObjectMeta: metav1.ObjectMeta{ 19 | Name: secret.Name, 20 | Namespace: namespace, 21 | }, 22 | Spec: LockboxSpec{ 23 | Sender: pub[:], 24 | Peer: peer[:], 25 | Namespace: encNS, 26 | Data: map[string][]byte{}, 27 | Template: LockboxSecretTemplate{ 28 | LockboxSecretTemplateMetadata: LockboxSecretTemplateMetadata{ 29 | Labels: secret.ObjectMeta.Labels, 30 | Annotations: secret.ObjectMeta.Annotations, 31 | }, 32 | Type: secret.Type, 33 | }, 34 | }, 35 | } 36 | 37 | for key, value := range secret.Data { 38 | enc := box.EasySeal(value, peer, pri) 39 | b.Spec.Data[key] = enc 40 | } 41 | 42 | for key, value := range secret.StringData { 43 | enc := box.EasySeal([]byte(value), peer, pri) 44 | b.Spec.Data[key] = enc 45 | } 46 | 47 | return b 48 | } 49 | 50 | // UnlockInto decrypts each secret value into the provided secret. 51 | func (in *Lockbox) UnlockInto(secret *corev1.Secret, pri nacl.Key) error { 52 | sender := new([keySize]byte) 53 | copy(sender[:], in.Spec.Sender) 54 | 55 | data := make(map[string][]byte, len(in.Spec.Data)) 56 | for key, val := range in.Spec.Data { 57 | d, err := box.EasyOpen(val, sender, pri) 58 | if err != nil { 59 | return decryptSecretKeyError{error: err, key: key} 60 | } 61 | 62 | data[key] = d 63 | } 64 | 65 | secret.Data = data 66 | secret.Type = in.Spec.Template.Type 67 | secret.Labels = in.Spec.Template.Labels 68 | secret.Annotations = in.Spec.Template.Annotations 69 | 70 | return nil 71 | } 72 | 73 | // decryptSecretKeyError wraps error while decrypting data from a secret. 74 | // This allows preserving the key for farther error messages. 75 | type decryptSecretKeyError struct { 76 | error 77 | key string 78 | } 79 | 80 | // SecretKey returns the secret data key that triggered this error. 81 | func (e decryptSecretKeyError) SecretKey() string { 82 | return e.key 83 | } 84 | 85 | // Unwrap implements Wrapper, returning the underlying error message. 86 | func (e decryptSecretKeyError) Unwrap() error { 87 | return e.error 88 | } 89 | 90 | func (in *Lockbox) GetConditions() []Condition { 91 | return in.Status.Conditions 92 | } 93 | 94 | func (in *Lockbox) SetConditions(conditions []Condition) { 95 | in.Status.Conditions = conditions 96 | } 97 | -------------------------------------------------------------------------------- /pkg/apis/lockbox.k8s.cloudflare.com/v1/lockbox_test.go: -------------------------------------------------------------------------------- 1 | package v1_test 2 | 3 | import ( 4 | "crypto/rand" 5 | "testing" 6 | 7 | v1 "github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare.com/v1" 8 | "github.com/kevinburke/nacl" 9 | "github.com/kevinburke/nacl/box" 10 | "gotest.tools/v3/assert" 11 | corev1 "k8s.io/api/core/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | ) 14 | 15 | func TestUnlock(t *testing.T) { 16 | _, priKey, err := loadKeypair(t, "6a42b9fc2b011fb88c01741483e3bffe455bdab1ae35d0bb53a3c00d406d8836", "252173f975f0a0ddb198a7e5958c074203a0e9f44275e0b840f95d456c4acc2e") 17 | assert.NilError(t, err) 18 | 19 | lb := &v1.Lockbox{ 20 | ObjectMeta: metav1.ObjectMeta{ 21 | Name: "example", 22 | Namespace: "example", 23 | Labels: map[string]string{ 24 | "type": "lockbox", 25 | }, 26 | Annotations: map[string]string{ 27 | "helm.sh/hook": "pre-install", 28 | }, 29 | }, 30 | Spec: v1.LockboxSpec{ 31 | Sender: []byte{0xb2, 0xa3, 0xf, 0x85, 0xa, 0x58, 0xcf, 0x94, 0x4c, 0x62, 0x37, 0xd4, 0xef, 0xf5, 0xed, 0x11, 0x52, 0xfa, 0x1b, 0xc3, 0xb0, 0x4d, 0x27, 0xd5, 0x58, 0x67, 0x61, 0x67, 0xe0, 0x10, 0xb1, 0x5c}, 32 | Peer: []byte{0x6a, 0x42, 0xb9, 0xfc, 0x2b, 0x1, 0x1f, 0xb8, 0x8c, 0x1, 0x74, 0x14, 0x83, 0xe3, 0xbf, 0xfe, 0x45, 0x5b, 0xda, 0xb1, 0xae, 0x35, 0xd0, 0xbb, 0x53, 0xa3, 0xc0, 0xd, 0x40, 0x6d, 0x88, 0x36}, 33 | Namespace: []byte{0x4d, 0xa0, 0x73, 0x8b, 0x95, 0xc3, 0xd4, 0x64, 0xe9, 0xab, 0xd, 0xb7, 0x1e, 0x5, 0x10, 0xed, 0x4c, 0x2f, 0x8a, 0x66, 0x6d, 0xec, 0x7c, 0x5d, 0x9b, 0xa7, 0xb7, 0x88, 0x49, 0x8a, 0xb9, 0x7f, 0xf0, 0x30, 0xe0, 0xad, 0x49, 0x7c, 0x3f, 0xe3, 0x1c, 0x2e, 0xe9, 0xb1, 0x2a, 0x70, 0x28}, 34 | Template: v1.LockboxSecretTemplate{ 35 | LockboxSecretTemplateMetadata: v1.LockboxSecretTemplateMetadata{ 36 | Labels: map[string]string{ 37 | "type": "secret", 38 | }, 39 | Annotations: map[string]string{ 40 | "wave": "ignore", 41 | }, 42 | }, 43 | }, 44 | Data: map[string][]byte{ 45 | "test": {0x7b, 0xca, 0x32, 0x90, 0xf7, 0x97, 0x3b, 0x6, 0xfb, 0x7c, 0xdc, 0x3a, 0x25, 0x82, 0x29, 0xdf, 0x9d, 0x1e, 0x46, 0x8d, 0xd4, 0x99, 0x49, 0x2, 0x63, 0x56, 0x54, 0x64, 0xae, 0x9e, 0xf2, 0xc0, 0x35, 0xf5, 0xf1, 0xcb, 0x67, 0xb7, 0xe2, 0xb1, 0x14, 0x42, 0x71, 0xc}, 46 | "test1": {0x2c, 0x68, 0xed, 0x53, 0x55, 0x55, 0xe2, 0x2d, 0x71, 0x96, 0x85, 0xfd, 0xdb, 0x93, 0x1e, 0x77, 0x91, 0x2d, 0x76, 0xba, 0xae, 0x46, 0x30, 0x9e, 0xb6, 0x65, 0xa2, 0x49, 0xfe, 0x78, 0xc0, 0xcb, 0x6d, 0xf, 0xa8, 0xeb, 0xa8, 0xfc, 0xc0, 0xa0, 0xdc, 0x4, 0x16, 0x7, 0xa0}, 47 | }, 48 | }, 49 | } 50 | 51 | secret := &corev1.Secret{} 52 | expected := &corev1.Secret{ 53 | ObjectMeta: metav1.ObjectMeta{ 54 | Labels: map[string]string{ 55 | "type": "secret", 56 | }, 57 | Annotations: map[string]string{ 58 | "wave": "ignore", 59 | }, 60 | }, 61 | Data: map[string][]byte{ 62 | "test": {0x74, 0x65, 0x73, 0x74}, 63 | "test1": {0x74, 0x65, 0x73, 0x74, 0x31}, 64 | }, 65 | } 66 | 67 | assert.NilError(t, lb.UnlockInto(secret, priKey)) 68 | assert.DeepEqual(t, secret, expected) 69 | } 70 | 71 | func TestUnlockErr(t *testing.T) { 72 | _, priKey, err := loadKeypair(t, "6a42b9fc2b011fb88c01741483e3bffe455bdab1ae35d0bb53a3c00d406d8836", "252173f975f0a0ddb198a7e5958c074203a0e9f44275e0b840f95d456c4acc2e") 73 | assert.NilError(t, err) 74 | 75 | lb := &v1.Lockbox{ 76 | ObjectMeta: metav1.ObjectMeta{ 77 | Name: "example", 78 | Namespace: "example", 79 | Labels: map[string]string{ 80 | "type": "lockbox", 81 | }, 82 | Annotations: map[string]string{ 83 | "helm.sh/hook": "pre-install", 84 | }, 85 | }, 86 | Spec: v1.LockboxSpec{ 87 | Sender: []byte{0x46, 0xa5, 0xfa, 0xa9, 0xe1, 0xe6, 0xd8, 0x48, 0x14, 0xfd, 0x52, 0x67, 0x98, 0x39, 0x12, 0xda, 0x78, 0x61, 0x95, 0x84, 0x8d, 0xbb, 0xd2, 0x1f, 0x36, 0xaa, 0xdc, 0x6a, 0x18, 0x5c, 0xe5, 0x77}, 88 | Peer: []byte{0x6a, 0x42, 0xb9, 0xfc, 0x2b, 0x01, 0x1f, 0xb8, 0x8c, 0x01, 0x74, 0x14, 0x83, 0xe3, 0xbf, 0xfe, 0x45, 0x5b, 0xda, 0xb1, 0xae, 0x35, 0xd0, 0xbb, 0x53, 0xa3, 0xc0, 0x0d, 0x40, 0x6d, 0x88, 0x36}, 89 | Namespace: []byte{0x4d, 0xa0, 0x73, 0x8b, 0x95, 0xc3, 0xd4, 0x64, 0xe9, 0xab, 0xd, 0xb7, 0x1e, 0x5, 0x10, 0xed, 0x4c, 0x2f, 0x8a, 0x66, 0x6d, 0xec, 0x7c, 0x5d, 0x9b, 0xa7, 0xb7, 0x88, 0x49, 0x8a, 0xb9, 0x7f, 0xf0, 0x30, 0xe0, 0xad, 0x49, 0x7c, 0x3f, 0xe3, 0x1c, 0x2e, 0xe9, 0xb1, 0x2a, 0x70, 0x28}, 90 | Template: v1.LockboxSecretTemplate{ 91 | LockboxSecretTemplateMetadata: v1.LockboxSecretTemplateMetadata{ 92 | Labels: map[string]string{ 93 | "type": "secret", 94 | }, 95 | Annotations: map[string]string{ 96 | "wave": "ignore", 97 | }, 98 | }, 99 | }, 100 | Data: map[string][]byte{ 101 | "test": {0x7b, 0xca, 0x32, 0x90, 0xf7, 0x97, 0x3b, 0x6, 0xfb, 0x7c, 0xdc, 0x3a, 0x25, 0x82, 0x29, 0xdf, 0x9d, 0x1e, 0x46, 0x8d, 0xd4, 0x99, 0x49, 0x2, 0x63, 0x56, 0x54, 0x64, 0xae, 0x9e, 0xf2, 0xc0, 0x35, 0xf5, 0xf1, 0xcb, 0x67, 0xb7, 0xe2, 0xb1, 0x14, 0x42, 0x71, 0xc}, 102 | "test1": {0x2c, 0x68, 0xed, 0x53, 0x55, 0x55, 0xe2, 0x2d, 0x71, 0x96, 0x85, 0xfd, 0xdb, 0x93, 0x1e, 0x77, 0x91, 0x2d, 0x76, 0xba, 0xae, 0x46, 0x30, 0x9e, 0xb6, 0x65, 0xa2, 0x49, 0xfe, 0x78, 0xc0, 0xcb, 0x6d, 0xf, 0xa8, 0xeb, 0xa8, 0xfc, 0xc0, 0xa0, 0xdc, 0x4, 0x16, 0x7, 0xa0}, 103 | }, 104 | }, 105 | } 106 | 107 | secret := &corev1.Secret{} 108 | expected := &corev1.Secret{} 109 | 110 | err = lb.UnlockInto(secret, priKey) 111 | assert.ErrorContains(t, err, "Could not decrypt invalid input") 112 | assert.DeepEqual(t, secret, expected) 113 | } 114 | 115 | func TestLockUnlock(t *testing.T) { 116 | senderPubKey, senderPriKey, _ := box.GenerateKey(rand.Reader) 117 | serverPubKey, serverPriKey, _ := box.GenerateKey(rand.Reader) 118 | 119 | secret := corev1.Secret{ 120 | ObjectMeta: metav1.ObjectMeta{ 121 | Labels: map[string]string{ 122 | "type": "secret", 123 | }, 124 | }, 125 | Data: map[string][]byte{ 126 | "test": {0x74, 0x65, 0x73, 0x74}, 127 | }, 128 | } 129 | 130 | lb := v1.NewFromSecret(secret, "namespace", serverPubKey, senderPubKey, senderPriKey) 131 | unlockedSecret := &corev1.Secret{} 132 | expectedSecret := &corev1.Secret{ 133 | ObjectMeta: metav1.ObjectMeta{ 134 | Labels: map[string]string{ 135 | "type": "secret", 136 | }, 137 | }, 138 | Data: map[string][]byte{ 139 | "test": {0x74, 0x65, 0x73, 0x74}, 140 | }, 141 | } 142 | 143 | assert.NilError(t, lb.UnlockInto(unlockedSecret, serverPriKey)) 144 | assert.DeepEqual(t, unlockedSecret, expectedSecret) 145 | } 146 | 147 | func loadKeypair(t *testing.T, pub, pri string) (pubKey, priKey nacl.Key, err error) { 148 | t.Helper() 149 | 150 | pubKey, err = nacl.Load(pub) 151 | if err != nil { 152 | return 153 | } 154 | 155 | priKey, err = nacl.Load(pri) 156 | return 157 | } 158 | -------------------------------------------------------------------------------- /pkg/apis/lockbox.k8s.cloudflare.com/v1/types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | ) 7 | 8 | // +kubebuilder:object:root=true 9 | // +kubebuilder:subresource:status 10 | // +kubebuilder:printcolumn:name="SecretType",type=string,JSONPath=`.spec.template.type` 11 | // +kubebuilder:printcolumn:name="Peer",type=string,JSONPath=`.spec.peer` 12 | 13 | // Lockbox is a struct wrapping the LockboxSpec in standard API server 14 | // metadata fields. 15 | type Lockbox struct { 16 | metav1.TypeMeta `json:",inline"` 17 | metav1.ObjectMeta `json:"metadata,omitempty"` 18 | 19 | // Desired state of the Lockbox resource. 20 | Spec LockboxSpec `json:"spec"` 21 | 22 | // Status of the Lockbox. This is set and managed automatically. 23 | // +optional 24 | Status LockboxStatus `json:"status,omitempty"` 25 | } 26 | 27 | // LockboxSpec is a struct wrapping the encrypted secrets along with the 28 | // public keys of the sender and server. 29 | type LockboxSpec struct { 30 | // Sender stores the public key used to lock this Lockbox. 31 | Sender []byte `json:"sender"` 32 | 33 | // Peer stores the public key that can unlock this Lockbox. 34 | Peer []byte `json:"peer"` 35 | 36 | // Namespace stores an encrypted copy of which namespace this Lockbox is locked 37 | // for, ensuring it cannot be deployed to another namespace under an attacker's 38 | // control. 39 | Namespace []byte `json:"namespace"` 40 | 41 | // Data contains the secret data, encrypted to the Peer's public key. Each key in the 42 | // data map must consist of alphanumeric characters, '-', '_', or '.'. 43 | Data map[string][]byte `json:"data"` 44 | 45 | // Template defines the structure of the Secret that will be 46 | // created from this Lockbox. 47 | // +optional 48 | Template LockboxSecretTemplate `json:"template,omitempty"` 49 | } 50 | 51 | // LockboxSecretTemplate defines structure of API metadata fields 52 | // of Secrets controlled by a Lockbox. 53 | type LockboxSecretTemplate struct { 54 | LockboxSecretTemplateMetadata `json:"metadata,omitempty"` 55 | 56 | // Type is used to facilitate programmatic handling of secret data. 57 | Type corev1.SecretType `json:"type,omitempty"` 58 | } 59 | 60 | type LockboxSecretTemplateMetadata struct { 61 | // Map of string keys and values that can be used to organize and categorize 62 | // (scope and select) objects. May match selectors of replication 63 | // controllers and services. More info: 64 | // http://kubernetes.io/docs/user-guide/labels 65 | // +optional 66 | Labels map[string]string `json:"labels,omitempty"` 67 | 68 | // Annotations is an unstructured key value map stored with a resource that 69 | // may be set by external tools to store and retrieve arbitrary metadata. 70 | // They are not queryable and should be preserved when modifying objects. 71 | // More info: http://kubernetes.io/docs/user-guide/annotations 72 | // +optional 73 | Annotations map[string]string `json:"annotations,omitempty"` 74 | } 75 | 76 | // LockboxStatus contains status information about a Lockbox. 77 | type LockboxStatus struct { 78 | // List of status conditions to indicate the status of a Lockbox. 79 | // +optional 80 | Conditions []Condition `json:"conditions,omitempty"` 81 | } 82 | 83 | // Condition contains condition information for a Lockbox. 84 | type Condition struct { 85 | // Type of condition in CamelCase. 86 | // +required 87 | Type ConditionType `json:"type"` 88 | 89 | // Status of the condition, one of True, False, Unknown 90 | // +required 91 | Status corev1.ConditionStatus `json:"status"` 92 | 93 | // Severity provides explicit classification of Reason code, so that users or machines 94 | // can immediately understand the current situation and act accordingly. 95 | // The Severity field MUST be set only when Status=False. 96 | // +optional 97 | Severity ConditionSeverity `json:"severity"` 98 | 99 | // LastTransitionTime marks when the condition last transitioned from one status to another. 100 | // This should be when the underlying condition changed. If that is not known, then using the time 101 | // when the API field changed is acceptable. 102 | // +required 103 | LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` 104 | 105 | // The reason for the condition's last transition in CamelCase. 106 | // +optional 107 | Reason string `json:"reason,omitempty"` 108 | 109 | // A message is the human readable message indicating details about the transition. 110 | // The field may be empty. 111 | // +optional 112 | Message string `json:"message,omitempty"` 113 | } 114 | 115 | // +kubebuilder:validation:Enum=Ready 116 | type ConditionType string 117 | 118 | const ( 119 | ReadyCondition ConditionType = "Ready" 120 | ) 121 | 122 | // +kubebuilder:validation:Enum=Error;Warning;Info 123 | type ConditionSeverity string 124 | 125 | const ( 126 | ConditionSeverityError ConditionSeverity = "Error" 127 | ConditionSeverityWarning ConditionSeverity = "Warning" 128 | ConditionSeverityInfo ConditionSeverity = "Info" 129 | ConditionSeverityNone ConditionSeverity = "" 130 | ) 131 | 132 | // +kubebuilder:object:root=true 133 | 134 | // LockboxList is a Lockbox-specific version of metav1.List. 135 | type LockboxList struct { 136 | metav1.TypeMeta 137 | metav1.ListMeta 138 | 139 | Items []Lockbox 140 | } 141 | -------------------------------------------------------------------------------- /pkg/apis/lockbox.k8s.cloudflare.com/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | // Code generated by controller-gen. DO NOT EDIT. 4 | 5 | package v1 6 | 7 | import ( 8 | runtime "k8s.io/apimachinery/pkg/runtime" 9 | ) 10 | 11 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 12 | func (in *Condition) DeepCopyInto(out *Condition) { 13 | *out = *in 14 | in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) 15 | } 16 | 17 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. 18 | func (in *Condition) DeepCopy() *Condition { 19 | if in == nil { 20 | return nil 21 | } 22 | out := new(Condition) 23 | in.DeepCopyInto(out) 24 | return out 25 | } 26 | 27 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 28 | func (in *Lockbox) DeepCopyInto(out *Lockbox) { 29 | *out = *in 30 | out.TypeMeta = in.TypeMeta 31 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 32 | in.Spec.DeepCopyInto(&out.Spec) 33 | in.Status.DeepCopyInto(&out.Status) 34 | } 35 | 36 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Lockbox. 37 | func (in *Lockbox) DeepCopy() *Lockbox { 38 | if in == nil { 39 | return nil 40 | } 41 | out := new(Lockbox) 42 | in.DeepCopyInto(out) 43 | return out 44 | } 45 | 46 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 47 | func (in *Lockbox) DeepCopyObject() runtime.Object { 48 | if c := in.DeepCopy(); c != nil { 49 | return c 50 | } 51 | return nil 52 | } 53 | 54 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 55 | func (in *LockboxList) DeepCopyInto(out *LockboxList) { 56 | *out = *in 57 | out.TypeMeta = in.TypeMeta 58 | in.ListMeta.DeepCopyInto(&out.ListMeta) 59 | if in.Items != nil { 60 | in, out := &in.Items, &out.Items 61 | *out = make([]Lockbox, len(*in)) 62 | for i := range *in { 63 | (*in)[i].DeepCopyInto(&(*out)[i]) 64 | } 65 | } 66 | } 67 | 68 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LockboxList. 69 | func (in *LockboxList) DeepCopy() *LockboxList { 70 | if in == nil { 71 | return nil 72 | } 73 | out := new(LockboxList) 74 | in.DeepCopyInto(out) 75 | return out 76 | } 77 | 78 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 79 | func (in *LockboxList) DeepCopyObject() runtime.Object { 80 | if c := in.DeepCopy(); c != nil { 81 | return c 82 | } 83 | return nil 84 | } 85 | 86 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 87 | func (in *LockboxSecretTemplate) DeepCopyInto(out *LockboxSecretTemplate) { 88 | *out = *in 89 | in.LockboxSecretTemplateMetadata.DeepCopyInto(&out.LockboxSecretTemplateMetadata) 90 | } 91 | 92 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LockboxSecretTemplate. 93 | func (in *LockboxSecretTemplate) DeepCopy() *LockboxSecretTemplate { 94 | if in == nil { 95 | return nil 96 | } 97 | out := new(LockboxSecretTemplate) 98 | in.DeepCopyInto(out) 99 | return out 100 | } 101 | 102 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 103 | func (in *LockboxSecretTemplateMetadata) DeepCopyInto(out *LockboxSecretTemplateMetadata) { 104 | *out = *in 105 | if in.Labels != nil { 106 | in, out := &in.Labels, &out.Labels 107 | *out = make(map[string]string, len(*in)) 108 | for key, val := range *in { 109 | (*out)[key] = val 110 | } 111 | } 112 | if in.Annotations != nil { 113 | in, out := &in.Annotations, &out.Annotations 114 | *out = make(map[string]string, len(*in)) 115 | for key, val := range *in { 116 | (*out)[key] = val 117 | } 118 | } 119 | } 120 | 121 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LockboxSecretTemplateMetadata. 122 | func (in *LockboxSecretTemplateMetadata) DeepCopy() *LockboxSecretTemplateMetadata { 123 | if in == nil { 124 | return nil 125 | } 126 | out := new(LockboxSecretTemplateMetadata) 127 | in.DeepCopyInto(out) 128 | return out 129 | } 130 | 131 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 132 | func (in *LockboxSpec) DeepCopyInto(out *LockboxSpec) { 133 | *out = *in 134 | if in.Sender != nil { 135 | in, out := &in.Sender, &out.Sender 136 | *out = make([]byte, len(*in)) 137 | copy(*out, *in) 138 | } 139 | if in.Peer != nil { 140 | in, out := &in.Peer, &out.Peer 141 | *out = make([]byte, len(*in)) 142 | copy(*out, *in) 143 | } 144 | if in.Namespace != nil { 145 | in, out := &in.Namespace, &out.Namespace 146 | *out = make([]byte, len(*in)) 147 | copy(*out, *in) 148 | } 149 | if in.Data != nil { 150 | in, out := &in.Data, &out.Data 151 | *out = make(map[string][]byte, len(*in)) 152 | for key, val := range *in { 153 | var outVal []byte 154 | if val == nil { 155 | (*out)[key] = nil 156 | } else { 157 | inVal := (*in)[key] 158 | in, out := &inVal, &outVal 159 | *out = make([]byte, len(*in)) 160 | copy(*out, *in) 161 | } 162 | (*out)[key] = outVal 163 | } 164 | } 165 | in.Template.DeepCopyInto(&out.Template) 166 | } 167 | 168 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LockboxSpec. 169 | func (in *LockboxSpec) DeepCopy() *LockboxSpec { 170 | if in == nil { 171 | return nil 172 | } 173 | out := new(LockboxSpec) 174 | in.DeepCopyInto(out) 175 | return out 176 | } 177 | 178 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 179 | func (in *LockboxStatus) DeepCopyInto(out *LockboxStatus) { 180 | *out = *in 181 | if in.Conditions != nil { 182 | in, out := &in.Conditions, &out.Conditions 183 | *out = make([]Condition, len(*in)) 184 | for i := range *in { 185 | (*in)[i].DeepCopyInto(&(*out)[i]) 186 | } 187 | } 188 | } 189 | 190 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LockboxStatus. 191 | func (in *LockboxStatus) DeepCopy() *LockboxStatus { 192 | if in == nil { 193 | return nil 194 | } 195 | out := new(LockboxStatus) 196 | in.DeepCopyInto(out) 197 | return out 198 | } 199 | -------------------------------------------------------------------------------- /pkg/flagvar/enum.go: -------------------------------------------------------------------------------- 1 | package flagvar 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | var ErrInvalidEnum = errors.New("invalid enum option") 10 | 11 | type Enum struct { 12 | Choices []string 13 | Value string 14 | } 15 | 16 | func (e *Enum) Help() string { 17 | return fmt.Sprintf("one of %v", e.Choices) 18 | } 19 | 20 | func (e *Enum) Set(v string) error { 21 | for _, c := range e.Choices { 22 | if strings.EqualFold(c, v) { 23 | e.Value = strings.ToLower(v) 24 | return nil 25 | } 26 | } 27 | 28 | return ErrInvalidEnum 29 | } 30 | 31 | func (e *Enum) String() string { 32 | if e == nil { 33 | return "" 34 | } 35 | 36 | return e.Value 37 | } 38 | -------------------------------------------------------------------------------- /pkg/flagvar/enum_test.go: -------------------------------------------------------------------------------- 1 | package flagvar_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cloudflare/lockbox/pkg/flagvar" 7 | "gotest.tools/v3/assert" 8 | ) 9 | 10 | func TestEnumString(t *testing.T) { 11 | type testCase struct { 12 | name string 13 | fv *flagvar.Enum 14 | expected string 15 | } 16 | 17 | run := func(t *testing.T, tc testCase) { 18 | actual := tc.fv.String() 19 | assert.Equal(t, actual, tc.expected) 20 | } 21 | 22 | testCases := []testCase{ 23 | { 24 | name: "non-nil receiver", 25 | fv: &flagvar.Enum{Value: "yaml"}, 26 | expected: "yaml", 27 | }, 28 | { 29 | name: "nil receiver", 30 | fv: nil, 31 | expected: "", 32 | }, 33 | } 34 | 35 | for _, tc := range testCases { 36 | tc := tc 37 | t.Run(tc.name, func(t *testing.T) { 38 | run(t, tc) 39 | }) 40 | } 41 | } 42 | 43 | func TestEnumSet(t *testing.T) { 44 | type testCase struct { 45 | name string 46 | input string 47 | expected string 48 | err error 49 | } 50 | 51 | run := func(t *testing.T, tc testCase) { 52 | fv := &flagvar.Enum{ 53 | Choices: []string{"yaml", "json"}, 54 | } 55 | err := fv.Set(tc.input) 56 | 57 | if err != nil { 58 | assert.ErrorIs(t, err, tc.err) 59 | } else { 60 | assert.Equal(t, fv.Value, tc.expected) 61 | } 62 | } 63 | 64 | testCases := []testCase{ 65 | { 66 | name: "valid enum option", 67 | input: "yaml", 68 | expected: "yaml", 69 | }, 70 | { 71 | name: "ignores option capitalization", 72 | input: "YaMl", 73 | expected: "yaml", 74 | }, 75 | { 76 | name: "invalid enum option", 77 | input: "cue", 78 | err: flagvar.ErrInvalidEnum, 79 | }, 80 | } 81 | 82 | for _, tc := range testCases { 83 | tc := tc 84 | t.Run(tc.name, func(t *testing.T) { 85 | run(t, tc) 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /pkg/flagvar/file.go: -------------------------------------------------------------------------------- 1 | package flagvar 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | // File is a flag.Value for file paths. Returns any errors from os.Stat. 8 | type File struct { 9 | Value string 10 | } 11 | 12 | // Help returns a string to include in the flag's help message. 13 | func (f *File) Help() string { 14 | return "file path" 15 | } 16 | 17 | // Set implements flag.Value by checking for the file's existence through 18 | // using os.Stat. Any error returned by os.Stat is returned by this function. 19 | func (f *File) Set(v string) error { 20 | _, err := os.Stat(v) 21 | f.Value = v 22 | 23 | return err 24 | } 25 | 26 | // String implements flag.Value by returning the current file path. 27 | func (f *File) String() string { 28 | if f == nil { 29 | return "" 30 | } 31 | 32 | return f.Value 33 | } 34 | 35 | // Type implements pflag.Value by noting our Value is string typed. 36 | func (f *File) Type() string { 37 | return "string" 38 | } 39 | -------------------------------------------------------------------------------- /pkg/flagvar/file_test.go: -------------------------------------------------------------------------------- 1 | package flagvar_test 2 | 3 | import ( 4 | "io/fs" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/cloudflare/lockbox/pkg/flagvar" 9 | "gotest.tools/v3/assert" 10 | ) 11 | 12 | func TestFileString(t *testing.T) { 13 | type testCase struct { 14 | name string 15 | fv *flagvar.File 16 | expected string 17 | } 18 | 19 | run := func(t *testing.T, tc testCase) { 20 | actual := tc.fv.String() 21 | assert.Equal(t, actual, tc.expected) 22 | } 23 | 24 | testCases := []testCase{ 25 | { 26 | name: "non-nil receiver", 27 | fv: &flagvar.File{Value: "/path/to/default.log"}, 28 | expected: "/path/to/default.log", 29 | }, 30 | { 31 | name: "nil receiver", 32 | fv: nil, 33 | expected: "", 34 | }, 35 | } 36 | 37 | for _, tc := range testCases { 38 | tc := tc 39 | t.Run(tc.name, func(t *testing.T) { 40 | run(t, tc) 41 | }) 42 | } 43 | } 44 | 45 | func TestFileSet(t *testing.T) { 46 | type testCase struct { 47 | name string 48 | input string 49 | expected string 50 | err error 51 | } 52 | 53 | run := func(t *testing.T, tc testCase) { 54 | fv := &flagvar.File{} 55 | 56 | err := fv.Set(tc.input) 57 | if tc.err != nil { 58 | assert.ErrorIs(t, err, tc.err) 59 | } else { 60 | assert.Equal(t, fv.Value, tc.expected) 61 | } 62 | } 63 | 64 | testCases := []testCase{ 65 | { 66 | name: "file exists", 67 | input: filepath.Join("testdata", "file"), 68 | expected: "testdata/file", 69 | }, 70 | { 71 | name: "file does not exist", 72 | input: filepath.Join("testdata", "file_nonexistant.go"), 73 | err: fs.ErrNotExist, 74 | }, 75 | } 76 | 77 | for _, tc := range testCases { 78 | tc := tc 79 | t.Run(tc.name, func(t *testing.T) { 80 | run(t, tc) 81 | }) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /pkg/flagvar/tcp_addr.go: -------------------------------------------------------------------------------- 1 | package flagvar 2 | 3 | import "net" 4 | 5 | // TCPAddr is a flag.Value for file paths. Returns any errors from net.ResolveTCPAddr. 6 | type TCPAddr struct { 7 | Network string 8 | Value *net.TCPAddr 9 | Text string 10 | } 11 | 12 | // Help returns a string to include in the flag's help message. 13 | func (t *TCPAddr) Help() string { 14 | return "TCP address in host:port format" 15 | } 16 | 17 | // Set implements flag.Value by parsing the provided address using net.ResolveTCPAddr. 18 | // Any error return is returned by this function. 19 | func (t *TCPAddr) Set(v string) error { 20 | network := "tcp" 21 | if t.Network != "" { 22 | network = t.Network 23 | } 24 | 25 | tcpAddr, err := net.ResolveTCPAddr(network, v) 26 | t.Text = v 27 | t.Value = tcpAddr 28 | 29 | return err 30 | } 31 | 32 | // String implements flag.Value by returning the current Text. 33 | func (t *TCPAddr) String() string { 34 | if t == nil { 35 | return "" 36 | } 37 | 38 | return t.Text 39 | } 40 | 41 | // Type implements pflag.Value by noting our Value is net.TCPAddr typed. 42 | func (t *TCPAddr) Type() string { 43 | return "net.TCPAddr" 44 | } 45 | -------------------------------------------------------------------------------- /pkg/flagvar/tcp_addr_test.go: -------------------------------------------------------------------------------- 1 | package flagvar_test 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/cloudflare/lockbox/pkg/flagvar" 8 | "gotest.tools/v3/assert" 9 | ) 10 | 11 | func TestTCPAddrString(t *testing.T) { 12 | type testCase struct { 13 | name string 14 | fv *flagvar.TCPAddr 15 | expected string 16 | } 17 | 18 | run := func(t *testing.T, tc testCase) { 19 | actual := tc.fv.String() 20 | assert.Equal(t, actual, tc.expected) 21 | } 22 | 23 | testCases := []testCase{ 24 | { 25 | name: "non-nil receiver", 26 | fv: &flagvar.TCPAddr{Text: ":8080"}, 27 | expected: ":8080", 28 | }, 29 | { 30 | name: "nil receiver", 31 | fv: nil, 32 | expected: "", 33 | }, 34 | } 35 | 36 | for _, tc := range testCases { 37 | tc := tc 38 | t.Run(tc.name, func(t *testing.T) { 39 | run(t, tc) 40 | }) 41 | } 42 | } 43 | 44 | func TestTCPAddrSet(t *testing.T) { 45 | type testCase struct { 46 | name string 47 | input string 48 | expected *net.TCPAddr 49 | err string 50 | } 51 | 52 | run := func(t *testing.T, tc testCase) { 53 | fv := flagvar.TCPAddr{} 54 | err := fv.Set(tc.input) 55 | 56 | if err != nil { 57 | assert.Error(t, err, tc.err) 58 | } else { 59 | assert.DeepEqual(t, fv.Value, tc.expected) 60 | } 61 | } 62 | 63 | testCases := []testCase{ 64 | { 65 | name: "host:port address", 66 | input: "127.0.0.1:8080", 67 | expected: &net.TCPAddr{ 68 | IP: net.ParseIP("127.0.0.1"), 69 | Port: 8080, 70 | }, 71 | }, 72 | { 73 | name: "port-only address", 74 | input: ":8080", 75 | expected: &net.TCPAddr{ 76 | Port: 8080, 77 | }, 78 | }, 79 | { 80 | name: "IPv6 support", 81 | input: "[::1]:8080", 82 | expected: &net.TCPAddr{ 83 | IP: net.ParseIP("::1"), 84 | Port: 8080, 85 | }, 86 | }, 87 | { 88 | name: "invalid address", 89 | input: "google.com", 90 | expected: nil, 91 | err: "address google.com: missing port in address", 92 | }, 93 | } 94 | 95 | for _, tc := range testCases { 96 | tc := tc 97 | t.Run(tc.name, func(t *testing.T) { 98 | run(t, tc) 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /pkg/flagvar/testdata/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/lockbox/b7bc038eb7db83fd5d130b963282826dc95667af/pkg/flagvar/testdata/file -------------------------------------------------------------------------------- /pkg/lockbox-controller/secretreconciler.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | 8 | lockboxv1 "github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare.com/v1" 9 | "github.com/cloudflare/lockbox/pkg/util/conditions" 10 | "github.com/kevinburke/nacl" 11 | "github.com/kevinburke/nacl/box" 12 | corev1 "k8s.io/api/core/v1" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/client-go/tools/record" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake" 17 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 18 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 19 | ) 20 | 21 | //go:generate controller-gen rbac:roleName=lockbox-controller paths=./. output:rbac:artifacts:config=../../deployment/rbac 22 | 23 | // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;patch;update 24 | // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch 25 | // +kubebuilder:rbac:groups="lockbox.k8s.cloudflare.com",resources=lockboxes,verbs=get;list;watch 26 | // +kubebuilder:rbac:groups="lockbox.k8s.cloudflare.com",resources=lockboxes/status,verbs=get;update;patch 27 | 28 | const keySize = nacl.KeySize 29 | 30 | // SecretReconcilerOption allows for functional options to modify the SecretReconciler 31 | type SecretReconcilerOption func(s *SecretReconciler) 32 | 33 | // SecretReconciler implements the reconciliation logic for Lockbox secrets. 34 | type SecretReconciler struct { 35 | pubKey, priKey nacl.Key 36 | 37 | client client.Client 38 | recorder record.EventRecorder 39 | } 40 | 41 | // NewSecretReconciler creates a reconciler controller for the provided keypair and options. 42 | // 43 | // If not mutated by any options, the reconciler uses a noop API client and events recorder. 44 | func NewSecretReconciler(pubKey, priKey nacl.Key, options ...SecretReconcilerOption) *SecretReconciler { 45 | sr := &SecretReconciler{ 46 | pubKey: pubKey, 47 | priKey: priKey, 48 | client: clientfake.NewClientBuilder().Build(), 49 | recorder: &record.FakeRecorder{}, 50 | } 51 | 52 | for _, opt := range options { 53 | opt(sr) 54 | } 55 | 56 | return sr 57 | } 58 | 59 | // Reconcile implements reconcile.Reconciler by ensuring Lockbox controlled Secrets are as described. 60 | func (s *SecretReconciler) Reconcile(ctx context.Context, lb *lockboxv1.Lockbox) (reconcile.Result, error) { 61 | if len(lb.Spec.Sender) != keySize { 62 | msg := fmt.Sprintf("invalid sender key length, got %d wanted %d", len(lb.Spec.Sender), keySize) 63 | 64 | s.recorder.Eventf(lb, "Warning", "InvalidKeyLength", msg) 65 | conditions.Set(lb, conditions.FalseCondition(lockboxv1.ReadyCondition, "InvalidKeyLength", lockboxv1.ConditionSeverityError, msg)) 66 | _ = s.client.Status().Update(ctx, lb) 67 | return reconcile.Result{}, fmt.Errorf("incorrect sender key length: %d, should be %d", len(lb.Spec.Sender), keySize) 68 | } 69 | if len(lb.Spec.Peer) != keySize { 70 | msg := fmt.Sprintf("invalid peer key length, got %d wanted %d", len(lb.Spec.Peer), keySize) 71 | 72 | s.recorder.Eventf(lb, "Warning", "InvalidKeyLength", msg) 73 | conditions.Set(lb, conditions.FalseCondition(lockboxv1.ReadyCondition, "InvalidKeyLength", lockboxv1.ConditionSeverityError, msg)) 74 | _ = s.client.Status().Update(ctx, lb) 75 | return reconcile.Result{}, fmt.Errorf("incorrect peer key length: %d, should be %d", len(lb.Spec.Peer), keySize) 76 | } 77 | 78 | peerKey := new([keySize]byte) 79 | copy(peerKey[:], lb.Spec.Peer) 80 | 81 | if !nacl.Verify32(peerKey, s.pubKey) { 82 | msg := fmt.Sprintf("lockbox has unknown peer key %q", base64.StdEncoding.EncodeToString(lb.Spec.Peer)) 83 | 84 | s.recorder.Eventf(lb, "Warning", "UnknownPeerKey", msg) 85 | conditions.Set(lb, conditions.FalseCondition(lockboxv1.ReadyCondition, "UnknownPeerKey", lockboxv1.ConditionSeverityError, msg)) 86 | _ = s.client.Status().Update(ctx, lb) 87 | return reconcile.Result{}, fmt.Errorf("unknown peer key") 88 | } 89 | 90 | sender := new([keySize]byte) 91 | copy(sender[:], lb.Spec.Sender) 92 | 93 | secret := &corev1.Secret{ 94 | ObjectMeta: metav1.ObjectMeta{ 95 | Name: lb.Name, 96 | Namespace: lb.Namespace, 97 | }, 98 | } 99 | 100 | namespace, err := box.EasyOpen(lb.Spec.Namespace, sender, s.priKey) 101 | if err != nil { 102 | msg := fmt.Sprintf("unable to open lockbox with peer key %q", base64.StdEncoding.EncodeToString(lb.Spec.Peer)) 103 | 104 | s.recorder.Eventf(lb, "Warning", "InvalidLockbox", msg) 105 | conditions.Set(lb, conditions.FalseCondition(lockboxv1.ReadyCondition, "InvalidLockbox", lockboxv1.ConditionSeverityError, msg)) 106 | _ = s.client.Status().Update(ctx, lb) 107 | return reconcile.Result{}, err 108 | } 109 | 110 | if string(namespace) != lb.Namespace { 111 | msg := fmt.Sprintf("locked for namespace %q, found in namespace %s", namespace, lb.Namespace) 112 | 113 | s.recorder.Eventf(lb, "Warning", "InvalidNamespace", msg) 114 | conditions.Set(lb, conditions.FalseCondition(lockboxv1.ReadyCondition, "InvalidNamespace", lockboxv1.ConditionSeverityWarning, msg)) 115 | _ = s.client.Status().Update(ctx, lb) 116 | return reconcile.Result{}, fmt.Errorf("incorrect namespace: %s, should be %s", namespace, lb.Namespace) 117 | } 118 | 119 | _, err = controllerutil.CreateOrPatch( 120 | ctx, 121 | s.client, 122 | secret, 123 | s.reconcileExisting(lb, sender, secret)) 124 | 125 | if err != nil { 126 | conditions.Set(lb, conditions.FalseCondition(lockboxv1.ReadyCondition, "InvalidLockbox", lockboxv1.ConditionSeverityWarning, err.Error())) 127 | _ = s.client.Status().Update(ctx, lb) 128 | return reconcile.Result{}, err 129 | } 130 | 131 | conditions.Set(lb, conditions.TrueCondition(lockboxv1.ReadyCondition)) 132 | _ = s.client.Status().Update(ctx, lb) 133 | return reconcile.Result{}, nil 134 | } 135 | 136 | // reconcileExisting returns a function suitable for controllerutil.CreateOrUpdate that mutates a Secret object 137 | // to reflect the desired state. 138 | func (s *SecretReconciler) reconcileExisting(lb *lockboxv1.Lockbox, sender nacl.Key, secret *corev1.Secret) func() error { 139 | return func() error { 140 | if err := controllerutil.SetControllerReference(lb, secret, s.client.Scheme()); err != nil { 141 | switch err := err.(type) { 142 | case decryptSecretKeyErrorer: 143 | s.recorder.Eventf(lb, "Warning", "InvalidLockbox", "lockbox contained key %q that could not be unlocked", err.SecretKey()) 144 | default: 145 | s.recorder.Eventf(lb, "Warning", "InvalidLockbox", "lockbox could not be unlocked") 146 | } 147 | 148 | return err 149 | } 150 | 151 | return lb.UnlockInto(secret, s.priKey) 152 | } 153 | } 154 | 155 | // WithRecorder sets the EventRecorder used by the SecretReconciler. 156 | func WithRecorder(r record.EventRecorder) SecretReconcilerOption { 157 | return func(s *SecretReconciler) { 158 | s.recorder = r 159 | } 160 | } 161 | 162 | // WithClient sets the API Client used by the SecretReconciler 163 | func WithClient(c client.Client) SecretReconcilerOption { 164 | return func(s *SecretReconciler) { 165 | s.client = c 166 | } 167 | } 168 | 169 | // decryptSecretKeyErrorer matches the unexported error type, to 170 | // fetch the secret data key that triggered the error. 171 | type decryptSecretKeyErrorer interface { 172 | SecretKey() string 173 | } 174 | -------------------------------------------------------------------------------- /pkg/lockbox-controller/secretreconciler_suite_test.go: -------------------------------------------------------------------------------- 1 | //go:build suite 2 | // +build suite 3 | 4 | package controller_test 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | "os" 11 | "path/filepath" 12 | "testing" 13 | 14 | lockboxv1 "github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare.com/v1" 15 | . "github.com/cloudflare/lockbox/pkg/lockbox-controller" 16 | "github.com/go-logr/zerologr" 17 | "github.com/google/go-cmp/cmp/cmpopts" 18 | "github.com/rs/zerolog" 19 | "gotest.tools/v3/assert" 20 | "gotest.tools/v3/poll" 21 | corev1 "k8s.io/api/core/v1" 22 | apierrors "k8s.io/apimachinery/pkg/api/errors" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/client-go/kubernetes/scheme" 25 | "k8s.io/client-go/rest" 26 | "k8s.io/utils/ptr" 27 | "sigs.k8s.io/controller-runtime/pkg/builder" 28 | "sigs.k8s.io/controller-runtime/pkg/client" 29 | "sigs.k8s.io/controller-runtime/pkg/envtest" 30 | logf "sigs.k8s.io/controller-runtime/pkg/log" 31 | "sigs.k8s.io/controller-runtime/pkg/manager" 32 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 33 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 34 | ) 35 | 36 | var cfg *rest.Config 37 | 38 | func TestMain(m *testing.M) { 39 | zl := zerolog.New(os.Stderr) 40 | logf.SetLogger(zerologr.New(&zl)) 41 | t := &envtest.Environment{ 42 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "deployment", "crds")}, 43 | } 44 | lockboxv1.AddToScheme(scheme.Scheme) 45 | 46 | var err error 47 | if cfg, err = t.Start(); err != nil { 48 | log.Fatal(err) 49 | } 50 | 51 | code := m.Run() 52 | t.Stop() 53 | os.Exit(code) 54 | } 55 | 56 | func TestSuiteSecretReconciler(t *testing.T) { 57 | type testCase struct { 58 | name string 59 | lockboxName string 60 | resources []client.Object 61 | expected *corev1.Secret 62 | } 63 | 64 | pubKey, priKey, err := loadKeypair(t, "6a42b9fc2b011fb88c01741483e3bffe455bdab1ae35d0bb53a3c00d406d8836", "252173f975f0a0ddb198a7e5958c074203a0e9f44275e0b840f95d456c4acc2e") 65 | assert.NilError(t, err) 66 | 67 | setup := func(t *testing.T, tc testCase) { 68 | mgr, err := manager.New(cfg, manager.Options{ 69 | Metrics: metricsserver.Options{ 70 | BindAddress: "0", 71 | }, 72 | Scheme: scheme.Scheme, 73 | }) 74 | assert.NilError(t, err) 75 | 76 | sr := NewSecretReconciler(pubKey, priKey, WithClient(mgr.GetClient())) 77 | err = builder. 78 | ControllerManagedBy(mgr). 79 | For(&lockboxv1.Lockbox{}). 80 | Owns(&corev1.Secret{}). 81 | Complete(reconcile.AsReconciler(mgr.GetClient(), sr)) 82 | assert.NilError(t, err) 83 | 84 | ctx, cancel := context.WithCancel(context.Background()) 85 | go func() { 86 | mgr.Start(ctx) 87 | }() 88 | t.Cleanup(cancel) 89 | } 90 | 91 | run := func(t *testing.T, tc testCase) { 92 | c, err := client.New(cfg, client.Options{}) 93 | assert.NilError(t, err) 94 | 95 | for _, r := range tc.resources { 96 | c.Create(context.Background(), r) 97 | } 98 | 99 | secret := &corev1.Secret{} 100 | poll.WaitOn(t, func(t poll.LogT) poll.Result { 101 | err := c.Get(context.Background(), client.ObjectKey{ 102 | Name: "example", 103 | Namespace: "default", 104 | }, secret) 105 | 106 | if err == nil { 107 | return poll.Success() 108 | } 109 | 110 | if apierrors.IsNotFound(err) { 111 | return poll.Continue("secret was not found") 112 | } 113 | 114 | return poll.Error(err) 115 | }) 116 | 117 | cm := &corev1.ConfigMap{} 118 | c.Get(context.Background(), client.ObjectKey{ 119 | Name: "example", 120 | Namespace: "default", 121 | }, cm) 122 | 123 | fmt.Printf("cm: %+v\n", *cm) 124 | 125 | assert.DeepEqual(t, secret, tc.expected, 126 | cmpopts.IgnoreFields(metav1.ObjectMeta{}, "UID", "ResourceVersion", "CreationTimestamp", "ManagedFields"), 127 | cmpopts.IgnoreFields(metav1.OwnerReference{}, "UID"), 128 | ) 129 | 130 | for _, r := range tc.resources { 131 | c.Delete(context.Background(), r) 132 | } 133 | // delete the created resource too, as there's no garbage collector 134 | c.Delete(context.Background(), tc.expected) 135 | } 136 | 137 | testCases := []testCase{ 138 | { 139 | name: "create secret", 140 | lockboxName: "example", 141 | resources: []client.Object{ 142 | &lockboxv1.Lockbox{ 143 | ObjectMeta: metav1.ObjectMeta{ 144 | Name: "example", 145 | Namespace: "default", 146 | Labels: map[string]string{ 147 | "type": "lockbox", 148 | }, 149 | Annotations: map[string]string{ 150 | "helm.sh/hook": "pre-install", 151 | }, 152 | }, 153 | Spec: lockboxv1.LockboxSpec{ 154 | Sender: []byte{0x74, 0xbd, 0xd8, 0x82, 0xf7, 0xd5, 0x87, 0xde, 0x08, 0x79, 0xf0, 0x9b, 0x35, 0x15, 0xf5, 0x2d, 0x1f, 0xb0, 0x26, 0xb3, 0x20, 0xe1, 0xe1, 0xd8, 0x5c, 0x5a, 0x0e, 0x1d, 0xfb, 0x80, 0x87, 0x23}, 155 | Peer: []byte{0x6a, 0x42, 0xb9, 0xfc, 0x2b, 0x01, 0x1f, 0xb8, 0x8c, 0x01, 0x74, 0x14, 0x83, 0xe3, 0xbf, 0xfe, 0x45, 0x5b, 0xda, 0xb1, 0xae, 0x35, 0xd0, 0xbb, 0x53, 0xa3, 0xc0, 0x0d, 0x40, 0x6d, 0x88, 0x36}, 156 | Namespace: []byte{0x3a, 0x1a, 0x82, 0xd1, 0xad, 0x9f, 0x89, 0x6b, 0x59, 0x8e, 0xce, 0x45, 0xbc, 0x6f, 0x61, 0x34, 0x81, 0x7b, 0x7e, 0x2f, 0xa4, 0xd7, 0x15, 0xaf, 0x28, 0x15, 0xc0, 0x3e, 0x21, 0xfc, 0xcb, 0x3a, 0x38, 0x60, 0x96, 0xc7, 0xac, 0xe6, 0x56, 0xf2, 0xb7, 0x40, 0x4e, 0x9e, 0xb4, 0xbf, 0x96}, 157 | Template: lockboxv1.LockboxSecretTemplate{ 158 | LockboxSecretTemplateMetadata: lockboxv1.LockboxSecretTemplateMetadata{ 159 | Labels: map[string]string{ 160 | "type": "secret", 161 | }, 162 | Annotations: map[string]string{ 163 | "wave": "ignore", 164 | }, 165 | }, 166 | }, 167 | Data: map[string][]byte{ 168 | "test": {0x57, 0x17, 0x83, 0x22, 0x4c, 0x54, 0x1a, 0xb8, 0x83, 0x86, 0xc6, 0x15, 0xed, 0x23, 0x10, 0x58, 0x1d, 0xbc, 0x20, 0x47, 0xb4, 0x2a, 0x7f, 0xf6, 0xda, 0x4e, 0xa4, 0x88, 0x6b, 0x54, 0xed, 0xf6, 0xa3, 0x21, 0x73, 0xda, 0xca, 0x2b, 0xf7, 0x88, 0x13, 0xaa, 0xc2, 0xef}, 169 | }, 170 | }, 171 | }, 172 | }, 173 | expected: &corev1.Secret{ 174 | ObjectMeta: metav1.ObjectMeta{ 175 | Name: "example", 176 | Namespace: "default", 177 | Labels: map[string]string{ 178 | "type": "secret", 179 | }, 180 | Annotations: map[string]string{ 181 | "wave": "ignore", 182 | }, 183 | OwnerReferences: []metav1.OwnerReference{ 184 | { 185 | APIVersion: "lockbox.k8s.cloudflare.com/v1", 186 | Kind: "Lockbox", 187 | Name: "example", 188 | Controller: ptr.To(true), 189 | BlockOwnerDeletion: ptr.To(true), 190 | }, 191 | }, 192 | }, 193 | Type: corev1.SecretTypeOpaque, 194 | Data: map[string][]byte{ 195 | "test": []byte("test"), 196 | }, 197 | }, 198 | }, 199 | { 200 | name: "avoids updating secrets owned by other controllers", 201 | lockboxName: "example", 202 | resources: []client.Object{ 203 | &corev1.Secret{ 204 | ObjectMeta: metav1.ObjectMeta{ 205 | Name: "example", 206 | Namespace: "default", 207 | OwnerReferences: []metav1.OwnerReference{ 208 | { 209 | APIVersion: "v1", 210 | Kind: "ConfigMap", 211 | Name: "example", 212 | UID: "deadbeef", 213 | Controller: ptr.To(true), 214 | BlockOwnerDeletion: ptr.To(true), 215 | }, 216 | }, 217 | }, 218 | Data: map[string][]byte{ 219 | "test": []byte("test"), 220 | "test1": []byte("test1"), 221 | }, 222 | }, 223 | &lockboxv1.Lockbox{ 224 | ObjectMeta: metav1.ObjectMeta{ 225 | Name: "example", 226 | Namespace: "default", 227 | }, 228 | Spec: lockboxv1.LockboxSpec{ 229 | Sender: []byte{0x74, 0xbd, 0xd8, 0x82, 0xf7, 0xd5, 0x87, 0xde, 0x08, 0x79, 0xf0, 0x9b, 0x35, 0x15, 0xf5, 0x2d, 0x1f, 0xb0, 0x26, 0xb3, 0x20, 0xe1, 0xe1, 0xd8, 0x5c, 0x5a, 0x0e, 0x1d, 0xfb, 0x80, 0x87, 0x23}, 230 | Peer: []byte{0x6a, 0x42, 0xb9, 0xfc, 0x2b, 0x01, 0x1f, 0xb8, 0x8c, 0x01, 0x74, 0x14, 0x83, 0xe3, 0xbf, 0xfe, 0x45, 0x5b, 0xda, 0xb1, 0xae, 0x35, 0xd0, 0xbb, 0x53, 0xa3, 0xc0, 0x0d, 0x40, 0x6d, 0x88, 0x36}, 231 | Namespace: []byte{0x3a, 0x1a, 0x82, 0xd1, 0xad, 0x9f, 0x89, 0x6b, 0x59, 0x8e, 0xce, 0x45, 0xbc, 0x6f, 0x61, 0x34, 0x81, 0x7b, 0x7e, 0x2f, 0xa4, 0xd7, 0x15, 0xaf, 0x28, 0x15, 0xc0, 0x3e, 0x21, 0xfc, 0xcb, 0x3a, 0x38, 0x60, 0x96, 0xc7, 0xac, 0xe6, 0x56, 0xf2, 0xb7, 0x40, 0x4e, 0x9e, 0xb4, 0xbf, 0x96}, 232 | Data: map[string][]byte{ 233 | "test": {0x57, 0x17, 0x83, 0x22, 0x4c, 0x54, 0x1a, 0xb8, 0x83, 0x86, 0xc6, 0x15, 0xed, 0x23, 0x10, 0x58, 0x1d, 0xbc, 0x20, 0x47, 0xb4, 0x2a, 0x7f, 0xf6, 0xda, 0x4e, 0xa4, 0x88, 0x6b, 0x54, 0xed, 0xf6, 0xa3, 0x21, 0x73, 0xda, 0xca, 0x2b, 0xf7, 0x88, 0x13, 0xaa, 0xc2, 0xef}, 234 | }, 235 | }, 236 | }, 237 | }, 238 | expected: &corev1.Secret{ 239 | ObjectMeta: metav1.ObjectMeta{ 240 | Name: "example", 241 | Namespace: "default", 242 | OwnerReferences: []metav1.OwnerReference{ 243 | { 244 | APIVersion: "v1", 245 | Kind: "ConfigMap", 246 | Name: "example", 247 | Controller: ptr.To(true), 248 | BlockOwnerDeletion: ptr.To(true), 249 | }, 250 | }, 251 | }, 252 | Type: corev1.SecretTypeOpaque, 253 | Data: map[string][]byte{ 254 | "test": []byte("test"), 255 | "test1": []byte("test1"), 256 | }, 257 | }, 258 | }, 259 | } 260 | 261 | for _, tc := range testCases { 262 | tc := tc 263 | t.Run(tc.name, func(t *testing.T) { 264 | setup(t, tc) 265 | run(t, tc) 266 | }) 267 | } 268 | 269 | } 270 | -------------------------------------------------------------------------------- /pkg/lockbox-controller/secretreconciler_test.go: -------------------------------------------------------------------------------- 1 | package controller_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | lockboxv1 "github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare.com/v1" 8 | controller "github.com/cloudflare/lockbox/pkg/lockbox-controller" 9 | "github.com/kevinburke/nacl" 10 | "gotest.tools/v3/assert" 11 | corev1 "k8s.io/api/core/v1" 12 | apierrors "k8s.io/apimachinery/pkg/api/errors" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | "k8s.io/apimachinery/pkg/types" 16 | "k8s.io/utils/ptr" 17 | "sigs.k8s.io/controller-runtime/pkg/client" 18 | clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake" 19 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 20 | ) 21 | 22 | func TestSecretReconciler(t *testing.T) { 23 | type testCase struct { 24 | name string 25 | lockboxName string 26 | resources []client.Object 27 | expected *corev1.Secret 28 | expectedErr string 29 | } 30 | 31 | run := func(t *testing.T, tc testCase) { 32 | scheme := runtime.NewScheme() 33 | assert.NilError(t, corev1.AddToScheme(scheme)) 34 | assert.NilError(t, lockboxv1.AddToScheme(scheme)) 35 | 36 | client := clientfake.NewClientBuilder(). 37 | WithObjects(tc.resources...). 38 | WithScheme(scheme). 39 | Build() 40 | 41 | pubKey, priKey, err := loadKeypair(t, "6a42b9fc2b011fb88c01741483e3bffe455bdab1ae35d0bb53a3c00d406d8836", "252173f975f0a0ddb198a7e5958c074203a0e9f44275e0b840f95d456c4acc2e") 42 | assert.NilError(t, err) 43 | 44 | lsn := types.NamespacedName{Name: tc.lockboxName, Namespace: "example"} 45 | sr := controller.NewSecretReconciler(pubKey, priKey, controller.WithClient(client)) 46 | 47 | _, err = reconcile.AsReconciler(client, sr).Reconcile(context.Background(), reconcile.Request{NamespacedName: lsn}) 48 | if tc.expectedErr != "" { 49 | assert.ErrorContains(t, err, tc.expectedErr) 50 | } else { 51 | assert.NilError(t, err) 52 | } 53 | 54 | actual := &corev1.Secret{} 55 | err = client.Get(context.Background(), lsn, actual) 56 | 57 | if tc.expected == nil { 58 | assert.Assert(t, apierrors.IsNotFound(err)) 59 | return 60 | } 61 | 62 | assert.NilError(t, err) 63 | assert.DeepEqual(t, actual, tc.expected) 64 | } 65 | 66 | testCases := []testCase{ 67 | { 68 | name: "new lockbox", 69 | lockboxName: "example", 70 | resources: []client.Object{ 71 | &lockboxv1.Lockbox{ 72 | ObjectMeta: metav1.ObjectMeta{ 73 | Name: "example", 74 | Namespace: "example", 75 | Labels: map[string]string{ 76 | "type": "lockbox", 77 | }, 78 | Annotations: map[string]string{ 79 | "helm.sh/hook": "pre-install", 80 | }, 81 | }, 82 | Spec: lockboxv1.LockboxSpec{ 83 | Sender: []byte{0xb2, 0xa3, 0xf, 0x85, 0xa, 0x58, 0xcf, 0x94, 0x4c, 0x62, 0x37, 0xd4, 0xef, 0xf5, 0xed, 0x11, 0x52, 0xfa, 0x1b, 0xc3, 0xb0, 0x4d, 0x27, 0xd5, 0x58, 0x67, 0x61, 0x67, 0xe0, 0x10, 0xb1, 0x5c}, 84 | Peer: []byte{0x6a, 0x42, 0xb9, 0xfc, 0x2b, 0x1, 0x1f, 0xb8, 0x8c, 0x1, 0x74, 0x14, 0x83, 0xe3, 0xbf, 0xfe, 0x45, 0x5b, 0xda, 0xb1, 0xae, 0x35, 0xd0, 0xbb, 0x53, 0xa3, 0xc0, 0xd, 0x40, 0x6d, 0x88, 0x36}, 85 | Namespace: []byte{0x4d, 0xa0, 0x73, 0x8b, 0x95, 0xc3, 0xd4, 0x64, 0xe9, 0xab, 0xd, 0xb7, 0x1e, 0x5, 0x10, 0xed, 0x4c, 0x2f, 0x8a, 0x66, 0x6d, 0xec, 0x7c, 0x5d, 0x9b, 0xa7, 0xb7, 0x88, 0x49, 0x8a, 0xb9, 0x7f, 0xf0, 0x30, 0xe0, 0xad, 0x49, 0x7c, 0x3f, 0xe3, 0x1c, 0x2e, 0xe9, 0xb1, 0x2a, 0x70, 0x28}, 86 | Template: lockboxv1.LockboxSecretTemplate{ 87 | LockboxSecretTemplateMetadata: lockboxv1.LockboxSecretTemplateMetadata{ 88 | Labels: map[string]string{ 89 | "type": "secret", 90 | }, 91 | Annotations: map[string]string{ 92 | "wave": "ignore", 93 | }, 94 | }, 95 | Type: corev1.SecretTypeOpaque, 96 | }, 97 | Data: map[string][]byte{ 98 | "test": {0x7b, 0xca, 0x32, 0x90, 0xf7, 0x97, 0x3b, 0x6, 0xfb, 0x7c, 0xdc, 0x3a, 0x25, 0x82, 0x29, 0xdf, 0x9d, 0x1e, 0x46, 0x8d, 0xd4, 0x99, 0x49, 0x2, 0x63, 0x56, 0x54, 0x64, 0xae, 0x9e, 0xf2, 0xc0, 0x35, 0xf5, 0xf1, 0xcb, 0x67, 0xb7, 0xe2, 0xb1, 0x14, 0x42, 0x71, 0xc}, 99 | "test1": {0x2c, 0x68, 0xed, 0x53, 0x55, 0x55, 0xe2, 0x2d, 0x71, 0x96, 0x85, 0xfd, 0xdb, 0x93, 0x1e, 0x77, 0x91, 0x2d, 0x76, 0xba, 0xae, 0x46, 0x30, 0x9e, 0xb6, 0x65, 0xa2, 0x49, 0xfe, 0x78, 0xc0, 0xcb, 0x6d, 0xf, 0xa8, 0xeb, 0xa8, 0xfc, 0xc0, 0xa0, 0xdc, 0x4, 0x16, 0x7, 0xa0}, 100 | }, 101 | }, 102 | }, 103 | }, 104 | expected: &corev1.Secret{ 105 | ObjectMeta: metav1.ObjectMeta{ 106 | Name: "example", 107 | Namespace: "example", 108 | Labels: map[string]string{ 109 | "type": "secret", 110 | }, 111 | Annotations: map[string]string{ 112 | "wave": "ignore", 113 | }, 114 | ResourceVersion: "1", 115 | OwnerReferences: []metav1.OwnerReference{ 116 | { 117 | APIVersion: "lockbox.k8s.cloudflare.com/v1", 118 | Kind: "Lockbox", 119 | Name: "example", 120 | Controller: ptr.To(true), 121 | BlockOwnerDeletion: ptr.To(true), 122 | }, 123 | }, 124 | }, 125 | Type: corev1.SecretTypeOpaque, 126 | Data: map[string][]byte{ 127 | "test": []byte("test"), 128 | "test1": []byte("test1"), 129 | }, 130 | }, 131 | }, 132 | { 133 | name: "update lockbox secret", 134 | lockboxName: "example", 135 | resources: []client.Object{ 136 | &lockboxv1.Lockbox{ 137 | ObjectMeta: metav1.ObjectMeta{ 138 | Name: "example", 139 | Namespace: "example", 140 | }, 141 | Spec: lockboxv1.LockboxSpec{ 142 | Sender: []byte{0xa, 0xda, 0x33, 0xf3, 0x48, 0xad, 0xb6, 0x4c, 0xaa, 0x6, 0x50, 0xc1, 0xe1, 0xa6, 0xeb, 0x49, 0x13, 0xe0, 0x53, 0xdf, 0xde, 0x44, 0x72, 0xd6, 0xe2, 0x51, 0x94, 0xee, 0xcb, 0xba, 0xc1, 0x4}, 143 | Peer: []byte{0x6a, 0x42, 0xb9, 0xfc, 0x2b, 0x1, 0x1f, 0xb8, 0x8c, 0x1, 0x74, 0x14, 0x83, 0xe3, 0xbf, 0xfe, 0x45, 0x5b, 0xda, 0xb1, 0xae, 0x35, 0xd0, 0xbb, 0x53, 0xa3, 0xc0, 0xd, 0x40, 0x6d, 0x88, 0x36}, 144 | Namespace: []byte{0xa7, 0x4c, 0x72, 0x7a, 0x71, 0x1d, 0x98, 0x32, 0xa, 0x3, 0xbe, 0xe5, 0x9d, 0xd4, 0x8c, 0x39, 0x3, 0x42, 0x9c, 0x5e, 0xeb, 0x6d, 0x95, 0x46, 0x5c, 0x10, 0x62, 0xa3, 0xa7, 0xfb, 0xee, 0x19, 0xcb, 0x98, 0xbf, 0xc1, 0x19, 0x66, 0x6a, 0x77, 0x76, 0x22, 0x17, 0x8f, 0xa5, 0x24, 0x8e}, 145 | Data: map[string][]byte{ 146 | "updated": {0x78, 0x70, 0x68, 0xae, 0x9f, 0xf5, 0xed, 0x60, 0x74, 0x14, 0x6a, 0xc5, 0xc3, 0xb, 0xe2, 0xaa, 0x20, 0x68, 0x7a, 0xfb, 0xa6, 0x6a, 0x38, 0xc2, 0x20, 0x73, 0xb5, 0x45, 0x9f, 0x9, 0xf0, 0x15, 0xd1, 0x5c, 0x16, 0x51, 0x50, 0xaa, 0xea, 0x68, 0x3a, 0x95, 0xe6}, 147 | }, 148 | Template: lockboxv1.LockboxSecretTemplate{ 149 | Type: corev1.SecretTypeOpaque, 150 | }, 151 | }, 152 | }, 153 | &corev1.Secret{ 154 | ObjectMeta: metav1.ObjectMeta{ 155 | Name: "example", 156 | Namespace: "example", 157 | Labels: map[string]string{ 158 | "type": "secret", 159 | }, 160 | Annotations: map[string]string{ 161 | "wave": "ignore", 162 | }, 163 | ResourceVersion: "1", 164 | OwnerReferences: []metav1.OwnerReference{ 165 | { 166 | APIVersion: "lockbox.k8s.cloudflare.com/v1", 167 | Kind: "Lockbox", 168 | Name: "example", 169 | Controller: ptr.To(true), 170 | BlockOwnerDeletion: ptr.To(true), 171 | }, 172 | }, 173 | }, 174 | Type: corev1.SecretTypeOpaque, 175 | Data: map[string][]byte{ 176 | "test": []byte("test"), 177 | "test1": []byte("test1"), 178 | }, 179 | }, 180 | }, 181 | expected: &corev1.Secret{ 182 | ObjectMeta: metav1.ObjectMeta{ 183 | Name: "example", 184 | Namespace: "example", 185 | ResourceVersion: "2", 186 | OwnerReferences: []metav1.OwnerReference{ 187 | { 188 | APIVersion: "lockbox.k8s.cloudflare.com/v1", 189 | Kind: "Lockbox", 190 | Name: "example", 191 | Controller: ptr.To(true), 192 | BlockOwnerDeletion: ptr.To(true), 193 | }, 194 | }, 195 | }, 196 | Type: corev1.SecretTypeOpaque, 197 | Data: map[string][]byte{ 198 | "updated": []byte("yep"), 199 | }, 200 | }, 201 | }, 202 | { 203 | name: "secret conflict", 204 | lockboxName: "example", 205 | resources: []client.Object{ 206 | &lockboxv1.Lockbox{ 207 | ObjectMeta: metav1.ObjectMeta{ 208 | Name: "example", 209 | Namespace: "example", 210 | }, 211 | Spec: lockboxv1.LockboxSpec{ 212 | Sender: []byte{0xa, 0xda, 0x33, 0xf3, 0x48, 0xad, 0xb6, 0x4c, 0xaa, 0x6, 0x50, 0xc1, 0xe1, 0xa6, 0xeb, 0x49, 0x13, 0xe0, 0x53, 0xdf, 0xde, 0x44, 0x72, 0xd6, 0xe2, 0x51, 0x94, 0xee, 0xcb, 0xba, 0xc1, 0x4}, 213 | Peer: []byte{0x6a, 0x42, 0xb9, 0xfc, 0x2b, 0x1, 0x1f, 0xb8, 0x8c, 0x1, 0x74, 0x14, 0x83, 0xe3, 0xbf, 0xfe, 0x45, 0x5b, 0xda, 0xb1, 0xae, 0x35, 0xd0, 0xbb, 0x53, 0xa3, 0xc0, 0xd, 0x40, 0x6d, 0x88, 0x36}, 214 | Namespace: []byte{0xa7, 0x4c, 0x72, 0x7a, 0x71, 0x1d, 0x98, 0x32, 0xa, 0x3, 0xbe, 0xe5, 0x9d, 0xd4, 0x8c, 0x39, 0x3, 0x42, 0x9c, 0x5e, 0xeb, 0x6d, 0x95, 0x46, 0x5c, 0x10, 0x62, 0xa3, 0xa7, 0xfb, 0xee, 0x19, 0xcb, 0x98, 0xbf, 0xc1, 0x19, 0x66, 0x6a, 0x77, 0x76, 0x22, 0x17, 0x8f, 0xa5, 0x24, 0x8e}, 215 | Data: map[string][]byte{ 216 | "updated": {0x78, 0x70, 0x68, 0xae, 0x9f, 0xf5, 0xed, 0x60, 0x74, 0x14, 0x6a, 0xc5, 0xc3, 0xb, 0xe2, 0xaa, 0x20, 0x68, 0x7a, 0xfb, 0xa6, 0x6a, 0x38, 0xc2, 0x20, 0x73, 0xb5, 0x45, 0x9f, 0x9, 0xf0, 0x15, 0xd1, 0x5c, 0x16, 0x51, 0x50, 0xaa, 0xea, 0x68, 0x3a, 0x95, 0xe6}, 217 | }, 218 | }, 219 | }, 220 | &corev1.Secret{ 221 | ObjectMeta: metav1.ObjectMeta{ 222 | Name: "example", 223 | Namespace: "example", 224 | ResourceVersion: "1", 225 | OwnerReferences: []metav1.OwnerReference{ 226 | { 227 | APIVersion: "bitnami.com/v1alpha1", 228 | Kind: "SealedSecret", 229 | Name: "example", 230 | Controller: ptr.To(true), 231 | BlockOwnerDeletion: ptr.To(true), 232 | }, 233 | }, 234 | }, 235 | Type: corev1.SecretTypeOpaque, 236 | Data: map[string][]byte{ 237 | "test": []byte("test"), 238 | "test1": []byte("test1"), 239 | }, 240 | }, 241 | }, 242 | expected: &corev1.Secret{ 243 | ObjectMeta: metav1.ObjectMeta{ 244 | Name: "example", 245 | Namespace: "example", 246 | ResourceVersion: "1", 247 | OwnerReferences: []metav1.OwnerReference{ 248 | { 249 | APIVersion: "bitnami.com/v1alpha1", 250 | Kind: "SealedSecret", 251 | Name: "example", 252 | Controller: ptr.To(true), 253 | BlockOwnerDeletion: ptr.To(true), 254 | }, 255 | }, 256 | }, 257 | Type: corev1.SecretTypeOpaque, 258 | Data: map[string][]byte{ 259 | "test": []byte("test"), 260 | "test1": []byte("test1"), 261 | }, 262 | }, 263 | expectedErr: "already owned by another SealedSecret controller example", 264 | }, 265 | { 266 | name: "docker-registry secret", 267 | lockboxName: "example", 268 | resources: []client.Object{ 269 | &lockboxv1.Lockbox{ 270 | ObjectMeta: metav1.ObjectMeta{ 271 | Name: "example", 272 | Namespace: "example", 273 | }, 274 | Spec: lockboxv1.LockboxSpec{ 275 | Sender: []byte{0x64, 0x22, 0x82, 0xe9, 0x35, 0x2a, 0x36, 0x7a, 0x40, 0x75, 0xd5, 0x14, 0xa6, 0x24, 0xef, 0xe3, 0x59, 0xda, 0xf5, 0xe9, 0xbe, 0xc8, 0x2d, 0x88, 0xb1, 0x17, 0xe8, 0xf2, 0x99, 0xb0, 0x9f, 0x71}, 276 | Peer: []byte{0x6a, 0x42, 0xb9, 0xfc, 0x2b, 0x1, 0x1f, 0xb8, 0x8c, 0x1, 0x74, 0x14, 0x83, 0xe3, 0xbf, 0xfe, 0x45, 0x5b, 0xda, 0xb1, 0xae, 0x35, 0xd0, 0xbb, 0x53, 0xa3, 0xc0, 0xd, 0x40, 0x6d, 0x88, 0x36}, 277 | Namespace: []byte{0xd3, 0x1c, 0xc6, 0x29, 0x65, 0xac, 0xd6, 0x5, 0x3a, 0x60, 0xe1, 0x7c, 0xf8, 0xb9, 0x7, 0xdd, 0xdb, 0xf0, 0x82, 0xab, 0x90, 0x38, 0x7, 0x56, 0x72, 0x68, 0xef, 0x56, 0x3b, 0xae, 0x13, 0x16, 0x7e, 0x3e, 0xf6, 0xaf, 0xb4, 0x7b, 0x10, 0xed, 0x77, 0x29, 0xae, 0xcb, 0x96, 0x7f, 0xc9}, 278 | Data: map[string][]byte{ 279 | ".dockerconfigjson": {0x98, 0x39, 0x7b, 0x93, 0x7a, 0xb6, 0x4, 0xc, 0xb8, 0x52, 0xf0, 0x97, 0x2e, 0x74, 0xed, 0xd6, 0x41, 0x7a, 0x7d, 0x20, 0xda, 0x35, 0x2d, 0xdf, 0x2b, 0x94, 0x9f, 0x78, 0x78, 0xd4, 0x29, 0x30, 0x6d, 0xbf, 0x9c, 0x59, 0x9f, 0xb4, 0x47, 0x5e, 0x10, 0x4a, 0xd2, 0xf, 0xd8, 0x77, 0x7d, 0x8, 0x11, 0x36, 0x41, 0xa9, 0xb2, 0x77, 0xac, 0xd9, 0xa3, 0x8, 0x81, 0x0, 0x6, 0x34, 0xde, 0x3e, 0xfc, 0x38, 0x4c, 0xa4, 0x27, 0xff, 0x1f, 0x67, 0x8, 0xef, 0x6, 0xff, 0x31, 0x80, 0xd, 0x4e, 0xcf, 0x6c, 0xec, 0x79, 0x78, 0x7d, 0x9f, 0x5b, 0x34, 0xe4, 0x5a, 0x44, 0x49, 0x57, 0xfd, 0xeb, 0x43, 0xd4, 0x4e, 0xe5, 0x15, 0xbc, 0xa8, 0x5a, 0x86, 0xd, 0xb9, 0xaa, 0x45, 0x6c, 0x4b, 0x17, 0x66, 0x13, 0xb2, 0x8c, 0x46, 0x7e, 0xdc, 0xe, 0x21, 0x54, 0x39, 0x27, 0xa3, 0x93, 0x52, 0x46, 0xa1, 0x71, 0x21, 0x8e, 0x27, 0x62, 0x6b, 0x86, 0xa6, 0xe4, 0x98, 0xc4, 0xff, 0x8, 0xed, 0xba, 0x4d, 0xa1, 0xfa, 0x53, 0x25, 0xb7, 0x29, 0x20, 0x1a, 0xab, 0x4a, 0xf5, 0x99, 0x99, 0x6a, 0x9d, 0xb8, 0x96, 0x28, 0x9b, 0x6a, 0xda, 0xb8, 0xee, 0x9c, 0x5f, 0xc1, 0x91, 0x0, 0x38, 0x84, 0x90, 0xdf, 0xbd, 0x9a, 0x1b, 0x9e, 0xd6, 0xe4, 0x3d}, 280 | }, 281 | Template: lockboxv1.LockboxSecretTemplate{ 282 | Type: corev1.SecretTypeDockerConfigJson, 283 | }, 284 | }, 285 | }, 286 | }, 287 | expected: &corev1.Secret{ 288 | ObjectMeta: metav1.ObjectMeta{ 289 | Name: "example", 290 | Namespace: "example", 291 | ResourceVersion: "1", 292 | OwnerReferences: []metav1.OwnerReference{ 293 | { 294 | APIVersion: "lockbox.k8s.cloudflare.com/v1", 295 | Kind: "Lockbox", 296 | Name: "example", 297 | Controller: ptr.To(true), 298 | BlockOwnerDeletion: ptr.To(true), 299 | }, 300 | }, 301 | }, 302 | Type: corev1.SecretTypeDockerConfigJson, 303 | Data: map[string][]byte{ 304 | ".dockerconfigjson": {0x65, 0x79, 0x4a, 0x68, 0x64, 0x58, 0x52, 0x6f, 0x63, 0x79, 0x49, 0x36, 0x65, 0x79, 0x4a, 0x6b, 0x62, 0x32, 0x4e, 0x72, 0x5a, 0x58, 0x49, 0x75, 0x5a, 0x58, 0x68, 0x68, 0x62, 0x58, 0x42, 0x73, 0x5a, 0x53, 0x35, 0x6a, 0x62, 0x32, 0x30, 0x69, 0x4f, 0x6e, 0x73, 0x69, 0x56, 0x58, 0x4e, 0x6c, 0x63, 0x6d, 0x35, 0x68, 0x62, 0x57, 0x55, 0x69, 0x4f, 0x69, 0x4a, 0x71, 0x62, 0x32, 0x56, 0x6b, 0x5a, 0x58, 0x5a, 0x6c, 0x62, 0x47, 0x39, 0x77, 0x5a, 0x58, 0x49, 0x69, 0x4c, 0x43, 0x4a, 0x51, 0x59, 0x58, 0x4e, 0x7a, 0x64, 0x32, 0x39, 0x79, 0x5a, 0x43, 0x49, 0x36, 0x49, 0x6e, 0x42, 0x68, 0x63, 0x33, 0x4e, 0x33, 0x62, 0x33, 0x4a, 0x6b, 0x49, 0x69, 0x77, 0x69, 0x52, 0x57, 0x31, 0x68, 0x61, 0x57, 0x77, 0x69, 0x4f, 0x69, 0x4a, 0x71, 0x62, 0x32, 0x56, 0x41, 0x5a, 0x58, 0x68, 0x68, 0x62, 0x58, 0x42, 0x73, 0x5a, 0x53, 0x35, 0x6a, 0x62, 0x32, 0x30, 0x69, 0x66, 0x58, 0x31, 0x39}, 305 | }, 306 | }, 307 | }, 308 | } 309 | 310 | for _, tc := range testCases { 311 | tc := tc 312 | t.Run(tc.name, func(t *testing.T) { 313 | run(t, tc) 314 | }) 315 | } 316 | } 317 | 318 | func loadKeypair(t *testing.T, pub, pri string) (pubKey, priKey nacl.Key, err error) { 319 | t.Helper() 320 | 321 | pubKey, err = nacl.Load(pub) 322 | if err != nil { 323 | return 324 | } 325 | 326 | priKey, err = nacl.Load(pri) 327 | return 328 | } 329 | -------------------------------------------------------------------------------- /pkg/lockbox-server/serve.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/kevinburke/nacl" 7 | ) 8 | 9 | // PublicKey creates an HTTP handler that responses with the specified public key 10 | // as binary data. 11 | func PublicKey(pubKey nacl.Key) http.Handler { 12 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 | w.Header().Set("Content-Type", "application/octet-stream") 14 | _, _ = w.Write(pubKey[:]) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/statemetrics/collector.go: -------------------------------------------------------------------------------- 1 | package statemetrics 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | "k8s.io/apimachinery/pkg/types" 8 | ) 9 | 10 | // Kubernetes is a metric tracking a numerical value that can arbitrary go up and down. 11 | type Kubernetes interface { 12 | prometheus.Metric 13 | prometheus.Collector 14 | 15 | // Set sets the tracked number to an arbitrary value. 16 | Set(float64) 17 | } 18 | 19 | // kubernetes implements Kubernetes. 20 | type kubernetes struct { 21 | prometheus.Metric 22 | values []string 23 | desc *prometheus.Desc 24 | } 25 | 26 | // Set creates a new constant metric for this numerical value 27 | func (k *kubernetes) Set(val float64) { 28 | k.Metric = prometheus.MustNewConstMetric(k.desc, prometheus.GaugeValue, val, k.values...) 29 | } 30 | 31 | // Describe implements Collector 32 | func (k *kubernetes) Describe(ch chan<- *prometheus.Desc) { 33 | ch <- k.desc 34 | } 35 | 36 | // Collect implements Collector 37 | func (k *kubernetes) Collect(ch chan<- prometheus.Metric) { 38 | ch <- k.Metric 39 | } 40 | 41 | // KubernetesVec is a Collector that bundles a set of Kubernetes metrics that all share the same Desc, 42 | // but have different values for their variable labels. This is used if you want to count the same 43 | // thing partitioned by various dimensions (e.g., namespaces, types). Create instances with 44 | // NewKubernetesVec. 45 | type KubernetesVec struct { 46 | desc *prometheus.Desc 47 | metrics map[types.UID]Kubernetes 48 | mu sync.Mutex 49 | } 50 | 51 | // KubernetesOpts is an alias for Opts. 52 | type KubernetesOpts prometheus.Opts 53 | 54 | // NewKubernetesVec creates a new KubernetesVec based on the provided KubernetesOpts and partitioned 55 | // by the given label names. 56 | func NewKubernetesVec(opts KubernetesOpts, labelNames []string) *KubernetesVec { 57 | desc := prometheus.NewDesc( 58 | prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), 59 | opts.Help, 60 | labelNames, 61 | opts.ConstLabels, 62 | ) 63 | 64 | return &KubernetesVec{ 65 | desc: desc, 66 | metrics: make(map[types.UID]Kubernetes), 67 | } 68 | } 69 | 70 | // WithLabelValues returns the Kubernetes metric for the given slice of label values (in the same order 71 | // as the variable labels). 72 | // 73 | // Consecutive calls for the same uid replace earlier metrics. 74 | func (v *KubernetesVec) WithLabelValues(uid types.UID, lvs ...string) Kubernetes { 75 | k := &kubernetes{ 76 | values: lvs, 77 | desc: v.desc, 78 | } 79 | k.Set(0) 80 | 81 | v.mu.Lock() 82 | v.metrics[uid] = k 83 | v.mu.Unlock() 84 | 85 | return k 86 | } 87 | 88 | // Delete deletes the metric stored for this uid. 89 | func (v *KubernetesVec) Delete(uid types.UID) { 90 | v.mu.Lock() 91 | defer v.mu.Unlock() 92 | 93 | delete(v.metrics, uid) 94 | } 95 | 96 | // Describe implements Collector. 97 | func (v *KubernetesVec) Describe(ch chan<- *prometheus.Desc) { 98 | ch <- v.desc 99 | } 100 | 101 | // Collect implements Collector. 102 | func (v *KubernetesVec) Collect(ch chan<- prometheus.Metric) { 103 | v.mu.Lock() 104 | defer v.mu.Unlock() 105 | 106 | for _, metric := range v.metrics { 107 | ch <- metric 108 | } 109 | } 110 | 111 | // LabelsVec is a Collector that bundles a set of unchecked Kubernetes metrics that all share the 112 | // same Desc, but have different labels. This is used if you want to count the same thing 113 | // partitioned by unbounded dimensions (e.g., Kubernetes labels). Create instances with 114 | // NewLabelsVec. 115 | type LabelsVec struct { 116 | opts KubernetesOpts 117 | metrics map[types.UID]Kubernetes 118 | mu sync.Mutex 119 | } 120 | 121 | // NewLabelsVec creates a new LabelsVec based on the provided KubernetesOpts. 122 | func NewLabelsVec(opts KubernetesOpts) *LabelsVec { 123 | return &LabelsVec{ 124 | opts: opts, 125 | metrics: make(map[types.UID]Kubernetes), 126 | } 127 | } 128 | 129 | // With returns the Kubernetes metric for the given Labels map. 130 | // 131 | // Consecutive calls with the same uid replace earlier metrics. 132 | func (v *LabelsVec) With(uid types.UID, l prometheus.Labels) Kubernetes { 133 | labels := make([]string, 0, len(l)) 134 | values := make([]string, 0, len(l)) 135 | 136 | for label, value := range l { 137 | labels = append(labels, label) 138 | values = append(values, value) 139 | } 140 | 141 | desc := prometheus.NewDesc( 142 | prometheus.BuildFQName(v.opts.Namespace, v.opts.Subsystem, v.opts.Name), 143 | v.opts.Help, 144 | labels, 145 | v.opts.ConstLabels, 146 | ) 147 | 148 | k := &kubernetes{ 149 | values: values, 150 | desc: desc, 151 | } 152 | k.Set(0) 153 | 154 | v.mu.Lock() 155 | v.metrics[uid] = k 156 | v.mu.Unlock() 157 | 158 | return k 159 | } 160 | 161 | // Delete deletes the metric stored for this uid. 162 | func (v *LabelsVec) Delete(uid types.UID) { 163 | v.mu.Lock() 164 | defer v.mu.Unlock() 165 | 166 | delete(v.metrics, uid) 167 | } 168 | 169 | // Describe implements Collector. No Desc are sent on the channel to make this 170 | // an unchecked collector. 171 | func (v *LabelsVec) Describe(chan<- *prometheus.Desc) {} 172 | 173 | // Collect implements Collector. 174 | func (v *LabelsVec) Collect(ch chan<- prometheus.Metric) { 175 | v.mu.Lock() 176 | defer v.mu.Unlock() 177 | 178 | for _, metric := range v.metrics { 179 | ch <- metric 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /pkg/statemetrics/handler.go: -------------------------------------------------------------------------------- 1 | package statemetrics 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | 7 | lockboxv1 "github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare.com/v1" 8 | "k8s.io/client-go/util/workqueue" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | "sigs.k8s.io/controller-runtime/pkg/event" 11 | "sigs.k8s.io/controller-runtime/pkg/handler" 12 | ) 13 | 14 | // StateMetricProxy updates state metrics by intercepting events as they are passed to event handlers. 15 | type StateMetricProxy struct { 16 | enqueuer handler.EventHandler 17 | 18 | info *KubernetesVec 19 | created *KubernetesVec 20 | resourceVersion *KubernetesVec 21 | lbType *KubernetesVec 22 | peerKey *KubernetesVec 23 | labels *LabelsVec 24 | } 25 | 26 | // NewStateMetricProxy returns a StateMetricsProxy. All metrics must be non-nil. 27 | func NewStateMetricProxy(enqueuer handler.EventHandler, info, created, resourceVersion, lbType, peerKey *KubernetesVec, labels *LabelsVec) *StateMetricProxy { 28 | return &StateMetricProxy{ 29 | enqueuer: enqueuer, 30 | info: info, 31 | created: created, 32 | resourceVersion: resourceVersion, 33 | lbType: lbType, 34 | peerKey: peerKey, 35 | labels: labels, 36 | } 37 | } 38 | 39 | // Create implements EventHandler. 40 | func (s *StateMetricProxy) Create(ctx context.Context, evt event.CreateEvent, q workqueue.RateLimitingInterface) { 41 | s.updateWith(evt.Object) 42 | 43 | if s.enqueuer != nil { 44 | s.enqueuer.Create(ctx, evt, q) 45 | } 46 | } 47 | 48 | // Update implements EventHandler. 49 | func (s *StateMetricProxy) Update(ctx context.Context, evt event.UpdateEvent, q workqueue.RateLimitingInterface) { 50 | s.updateWith(evt.ObjectNew) 51 | 52 | if s.enqueuer != nil { 53 | s.enqueuer.Update(ctx, evt, q) 54 | } 55 | } 56 | 57 | // Delete implements EventHandler. 58 | func (s *StateMetricProxy) Delete(ctx context.Context, evt event.DeleteEvent, q workqueue.RateLimitingInterface) { 59 | uid := evt.Object.GetUID() 60 | 61 | s.info.Delete(uid) 62 | s.created.Delete(uid) 63 | s.resourceVersion.Delete(uid) 64 | s.lbType.Delete(uid) 65 | s.peerKey.Delete(uid) 66 | s.labels.Delete(uid) 67 | 68 | if s.enqueuer != nil { 69 | s.enqueuer.Delete(ctx, evt, q) 70 | } 71 | } 72 | 73 | // Generic implements EventHandler. 74 | func (s *StateMetricProxy) Generic(ctx context.Context, evt event.GenericEvent, q workqueue.RateLimitingInterface) { 75 | if s.enqueuer != nil { 76 | s.enqueuer.Generic(ctx, evt, q) 77 | } 78 | } 79 | 80 | // updateWith updates the metrics for Create and Update handles. 81 | func (s *StateMetricProxy) updateWith(obj client.Object) { 82 | namespace := obj.GetNamespace() 83 | lockbox := obj.GetName() 84 | uid := obj.GetUID() 85 | 86 | s.info.WithLabelValues(uid, namespace, lockbox).Set(1) 87 | creationTime := obj.GetCreationTimestamp() 88 | if !creationTime.IsZero() { 89 | s.created.WithLabelValues(uid, namespace, lockbox).Set(float64(creationTime.Unix())) 90 | } 91 | s.resourceVersion.WithLabelValues(uid, namespace, lockbox, obj.GetResourceVersion()).Set(1) 92 | 93 | if lb, ok := obj.(*lockboxv1.Lockbox); ok { 94 | s.lbType.WithLabelValues(uid, namespace, lockbox, string(lb.Spec.Template.Type)).Set(1) 95 | s.peerKey.WithLabelValues(uid, namespace, lockbox, hex.EncodeToString(lb.Spec.Peer)).Set(1) 96 | } 97 | 98 | promLabels := kubernetesLabelsToPrometheusLabels(obj.GetLabels()) 99 | promLabels["namespace"] = namespace 100 | promLabels["lockbox"] = lockbox 101 | 102 | s.labels.With(uid, promLabels).Set(1) 103 | } 104 | -------------------------------------------------------------------------------- /pkg/statemetrics/handler_test.go: -------------------------------------------------------------------------------- 1 | package statemetrics 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | lockboxv1 "github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare.com/v1" 10 | "github.com/prometheus/client_golang/prometheus" 11 | "github.com/prometheus/client_golang/prometheus/testutil" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "sigs.k8s.io/controller-runtime/pkg/event" 14 | ) 15 | 16 | func TestStateMetricsProxy_Create(t *testing.T) { 17 | lb := &lockboxv1.Lockbox{ 18 | ObjectMeta: metav1.ObjectMeta{ 19 | Namespace: "fizz", 20 | Name: "buzz", 21 | UID: "foobar", 22 | ResourceVersion: "9001", 23 | CreationTimestamp: metav1.Date(2001, time.September, 9, 1, 46, 40, 0, time.UTC), 24 | Labels: map[string]string{ 25 | "testing": "true", 26 | }, 27 | }, 28 | Spec: lockboxv1.LockboxSpec{ 29 | Peer: []byte{0xDE, 0xAD, 0xBE, 0xEF}, 30 | Template: lockboxv1.LockboxSecretTemplate{ 31 | Type: "golang.org/testing", 32 | }, 33 | }, 34 | } 35 | info, created, resourceVersion, lbType, peerKey, labels := createMetricVectors(t) 36 | 37 | reg := prometheus.NewPedanticRegistry() 38 | reg.MustRegister(info, created, resourceVersion, lbType, peerKey, labels) 39 | 40 | evt := event.CreateEvent{Object: lb} 41 | 42 | handler := NewStateMetricProxy(nil, info, created, resourceVersion, lbType, peerKey, labels) 43 | handler.Create(context.Background(), evt, nil) 44 | 45 | expected := strings.NewReader(` 46 | # HELP kube_lockbox_info Information about Lockbox 47 | # TYPE kube_lockbox_info gauge 48 | kube_lockbox_info{lockbox="buzz",namespace="fizz"} 1 49 | # HELP kube_lockbox_created Unix creation timestamp 50 | # TYPE kube_lockbox_created gauge 51 | kube_lockbox_created{lockbox="buzz",namespace="fizz"} 1e9 52 | # HELP kube_lockbox_resource_version Resource version representing a specific version of a Lockbox 53 | # TYPE kube_lockbox_resource_version gauge 54 | kube_lockbox_resource_version{lockbox="buzz",namespace="fizz",resource_version="9001"} 1 55 | # HELP kube_lockbox_type Lockbox secret type 56 | # TYPE kube_lockbox_type gauge 57 | kube_lockbox_type{lockbox="buzz",namespace="fizz",type="golang.org/testing"} 1 58 | # HELP kube_lockbox_peer Lockbox peer key 59 | # TYPE kube_lockbox_peer gauge 60 | kube_lockbox_peer{lockbox="buzz",namespace="fizz",peer="deadbeef"} 1 61 | # HELP kube_lockbox_labels Kubernetes labels converted to Prometheus labels 62 | # TYPE kube_lockbox_labels gauge 63 | kube_lockbox_labels{label_testing="true",lockbox="buzz",namespace="fizz"} 1 64 | `) 65 | 66 | if err := testutil.GatherAndCompare(reg, expected); err != nil { 67 | t.Error(err) 68 | } 69 | } 70 | 71 | func TestStateMetricsProxy_Update(t *testing.T) { 72 | old := &lockboxv1.Lockbox{ 73 | ObjectMeta: metav1.ObjectMeta{ 74 | Namespace: "fizz", 75 | Name: "buzz", 76 | UID: "foobar", 77 | ResourceVersion: "8999", 78 | CreationTimestamp: metav1.Now(), 79 | Labels: map[string]string{ 80 | "testing": "false", 81 | }, 82 | }, 83 | Spec: lockboxv1.LockboxSpec{ 84 | Peer: []byte{0x00}, 85 | Template: lockboxv1.LockboxSecretTemplate{ 86 | Type: "example.org/old", 87 | }, 88 | }, 89 | } 90 | lb := &lockboxv1.Lockbox{ 91 | ObjectMeta: metav1.ObjectMeta{ 92 | Namespace: "fizz", 93 | Name: "buzz", 94 | UID: "foobar", 95 | ResourceVersion: "9001", 96 | CreationTimestamp: metav1.Date(2001, time.September, 9, 1, 46, 40, 0, time.UTC), 97 | Labels: map[string]string{ 98 | "testing": "true", 99 | }, 100 | }, 101 | Spec: lockboxv1.LockboxSpec{ 102 | Peer: []byte{0xDE, 0xAD, 0xBE, 0xEF}, 103 | Template: lockboxv1.LockboxSecretTemplate{ 104 | Type: "golang.org/testing", 105 | }, 106 | }, 107 | } 108 | info, created, resourceVersion, lbType, peerKey, labels := createMetricVectors(t) 109 | 110 | reg := prometheus.NewPedanticRegistry() 111 | reg.MustRegister(info, created, resourceVersion, lbType, peerKey, labels) 112 | 113 | create := event.CreateEvent{Object: old} 114 | upd := event.UpdateEvent{ 115 | ObjectOld: old, 116 | ObjectNew: lb, 117 | } 118 | 119 | handler := NewStateMetricProxy(nil, info, created, resourceVersion, lbType, peerKey, labels) 120 | handler.Create(context.Background(), create, nil) 121 | handler.Update(context.Background(), upd, nil) 122 | 123 | expected := strings.NewReader(` 124 | # HELP kube_lockbox_info Information about Lockbox 125 | # TYPE kube_lockbox_info gauge 126 | kube_lockbox_info{lockbox="buzz",namespace="fizz"} 1 127 | # HELP kube_lockbox_created Unix creation timestamp 128 | # TYPE kube_lockbox_created gauge 129 | kube_lockbox_created{lockbox="buzz",namespace="fizz"} 1e9 130 | # HELP kube_lockbox_resource_version Resource version representing a specific version of a Lockbox 131 | # TYPE kube_lockbox_resource_version gauge 132 | kube_lockbox_resource_version{lockbox="buzz",namespace="fizz",resource_version="9001"} 1 133 | # HELP kube_lockbox_type Lockbox secret type 134 | # TYPE kube_lockbox_type gauge 135 | kube_lockbox_type{lockbox="buzz",namespace="fizz",type="golang.org/testing"} 1 136 | # HELP kube_lockbox_peer Lockbox peer key 137 | # TYPE kube_lockbox_peer gauge 138 | kube_lockbox_peer{lockbox="buzz",namespace="fizz",peer="deadbeef"} 1 139 | # HELP kube_lockbox_labels Kubernetes labels converted to Prometheus labels 140 | # TYPE kube_lockbox_labels gauge 141 | kube_lockbox_labels{label_testing="true",lockbox="buzz",namespace="fizz"} 1 142 | `) 143 | 144 | if err := testutil.GatherAndCompare(reg, expected); err != nil { 145 | t.Error(err) 146 | } 147 | } 148 | 149 | func TestStateMetricsProxy_Delete(t *testing.T) { 150 | lb := &lockboxv1.Lockbox{ 151 | ObjectMeta: metav1.ObjectMeta{ 152 | Namespace: "fizz", 153 | Name: "buzz", 154 | UID: "foobar", 155 | ResourceVersion: "9001", 156 | CreationTimestamp: metav1.Date(2001, time.September, 9, 1, 46, 40, 0, time.UTC), 157 | Labels: map[string]string{ 158 | "testing": "true", 159 | }, 160 | }, 161 | Spec: lockboxv1.LockboxSpec{ 162 | Peer: []byte{0xDE, 0xAD, 0xBE, 0xEF}, 163 | Template: lockboxv1.LockboxSecretTemplate{ 164 | Type: "golang.org/testing", 165 | }, 166 | }, 167 | } 168 | info, created, resourceVersion, lbType, peerKey, labels := createMetricVectors(t) 169 | 170 | reg := prometheus.NewPedanticRegistry() 171 | reg.MustRegister(info, created, resourceVersion, lbType, peerKey, labels) 172 | 173 | create := event.CreateEvent{Object: lb} 174 | deleted := event.DeleteEvent{ 175 | Object: lb, 176 | DeleteStateUnknown: false, 177 | } 178 | 179 | handler := NewStateMetricProxy(nil, info, created, resourceVersion, lbType, peerKey, labels) 180 | handler.Create(context.Background(), create, nil) 181 | handler.Delete(context.Background(), deleted, nil) 182 | 183 | expected := &strings.Reader{} 184 | 185 | if err := testutil.GatherAndCompare(reg, expected); err != nil { 186 | t.Error(err) 187 | } 188 | } 189 | 190 | func createMetricVectors(t *testing.T) (info, created, resourceVersion, lbType, peerKey *KubernetesVec, labels *LabelsVec) { 191 | info = NewKubernetesVec(KubernetesOpts{ 192 | Name: "kube_lockbox_info", 193 | Help: "Information about Lockbox", 194 | }, []string{"namespace", "lockbox"}) 195 | created = NewKubernetesVec(KubernetesOpts{ 196 | Name: "kube_lockbox_created", 197 | Help: "Unix creation timestamp", 198 | }, []string{"namespace", "lockbox"}) 199 | resourceVersion = NewKubernetesVec(KubernetesOpts{ 200 | Name: "kube_lockbox_resource_version", 201 | Help: "Resource version representing a specific version of a Lockbox", 202 | }, []string{"namespace", "lockbox", "resource_version"}) 203 | lbType = NewKubernetesVec(KubernetesOpts{ 204 | Name: "kube_lockbox_type", 205 | Help: "Lockbox secret type", 206 | }, []string{"namespace", "lockbox", "type"}) 207 | peerKey = NewKubernetesVec(KubernetesOpts{ 208 | Name: "kube_lockbox_peer", 209 | Help: "Lockbox peer key", 210 | }, []string{"namespace", "lockbox", "peer"}) 211 | labels = NewLabelsVec(KubernetesOpts{ 212 | Name: "kube_lockbox_labels", 213 | Help: "Kubernetes labels converted to Prometheus labels", 214 | }) 215 | 216 | return 217 | } 218 | -------------------------------------------------------------------------------- /pkg/statemetrics/labels.go: -------------------------------------------------------------------------------- 1 | package statemetrics 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | var invalidLabelCharRE = regexp.MustCompile(`[^a-zA-Z0-9_]`) 10 | 11 | // sanitizeLabel replaces non-alphanumeric characters with underscores. 12 | func sanitizeLabel(l string) string { 13 | return invalidLabelCharRE.ReplaceAllString(l, "_") 14 | } 15 | 16 | // kubernetesLabelsToPrometheusLabels generates Prometheus-safe labels from 17 | // the resource's Kubernetes labels. 18 | func kubernetesLabelsToPrometheusLabels(labels map[string]string) prometheus.Labels { 19 | promLabels := map[string]string{} 20 | for l, v := range labels { 21 | promLabels["label_"+sanitizeLabel(l)] = v 22 | } 23 | 24 | return promLabels 25 | } 26 | -------------------------------------------------------------------------------- /pkg/statemetrics/labels_test.go: -------------------------------------------------------------------------------- 1 | package statemetrics 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "testing/quick" 7 | 8 | "github.com/prometheus/common/model" 9 | ) 10 | 11 | const reservedLabelPrefix = "__" 12 | 13 | func TestLabelsTransformation(t *testing.T) { 14 | f := func(labels map[string]string) bool { 15 | newLabels := kubernetesLabelsToPrometheusLabels(labels) 16 | 17 | for k := range newLabels { 18 | if !checkLabelName(k) { 19 | return false 20 | } 21 | } 22 | 23 | return true 24 | } 25 | 26 | if err := quick.Check(f, nil); err != nil { 27 | t.Error(err) 28 | } 29 | } 30 | 31 | func checkLabelName(l string) bool { 32 | return model.LabelName(l).IsValid() && !strings.HasPrefix(l, reservedLabelPrefix) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/util/conditions/conditions.go: -------------------------------------------------------------------------------- 1 | // Adapted from https://github.com/kubernetes-sigs/cluster-api/tree/v0.3.10/util/conditions 2 | // 3 | // Copyright 2020 The Kubernetes Authors. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | // Package conditions provides functions for setting status conditions on Lockbox resources 18 | package conditions 19 | 20 | import ( 21 | "sort" 22 | "time" 23 | 24 | lockboxv1 "github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare.com/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | ) 27 | 28 | type Getter interface { 29 | GetConditions() []lockboxv1.Condition 30 | } 31 | 32 | type Setter interface { 33 | Getter 34 | SetConditions([]lockboxv1.Condition) 35 | } 36 | 37 | func FalseCondition(t lockboxv1.ConditionType, reason string, severity lockboxv1.ConditionSeverity, message string) *lockboxv1.Condition { 38 | return &lockboxv1.Condition{ 39 | Type: t, 40 | Status: "False", 41 | Reason: reason, 42 | Severity: severity, 43 | Message: message, 44 | } 45 | } 46 | 47 | func TrueCondition(t lockboxv1.ConditionType) *lockboxv1.Condition { 48 | return &lockboxv1.Condition{ 49 | Type: t, 50 | Status: "True", 51 | } 52 | } 53 | 54 | func UnknownCondition(t lockboxv1.ConditionType, reason string, message string) *lockboxv1.Condition { 55 | return &lockboxv1.Condition{ 56 | Type: t, 57 | Status: "Unknown", 58 | Reason: reason, 59 | Message: message, 60 | } 61 | } 62 | 63 | func Get(from Getter, t lockboxv1.ConditionType) *lockboxv1.Condition { 64 | conditions := from.GetConditions() 65 | if conditions == nil { 66 | return nil 67 | } 68 | 69 | for _, condition := range conditions { 70 | if condition.Type == t { 71 | return &condition 72 | } 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func Set(to Setter, condition *lockboxv1.Condition) { 79 | if to == nil || condition == nil { 80 | return 81 | } 82 | 83 | conditions := to.GetConditions() 84 | exists := false 85 | for i := range conditions { 86 | existingCondition := conditions[i] 87 | if existingCondition.Type == condition.Type { 88 | exists = true 89 | if !hasSameState(&existingCondition, condition) { 90 | condition.LastTransitionTime = metav1.NewTime(time.Now().UTC().Truncate(time.Second)) 91 | conditions[i] = *condition 92 | break 93 | } 94 | condition.LastTransitionTime = existingCondition.LastTransitionTime 95 | break 96 | } 97 | } 98 | 99 | if !exists { 100 | if condition.LastTransitionTime.IsZero() { 101 | condition.LastTransitionTime = metav1.NewTime(time.Now().UTC().Truncate(time.Second)) 102 | } 103 | conditions = append(conditions, *condition) 104 | } 105 | 106 | sort.Slice(conditions, func(i, j int) bool { 107 | return lexicographicLess(&conditions[i], &conditions[j]) 108 | }) 109 | 110 | to.SetConditions(conditions) 111 | } 112 | 113 | func lexicographicLess(i, j *lockboxv1.Condition) bool { 114 | return (i.Type == "Ready" || i.Type < j.Type) && j.Type != "Ready" 115 | } 116 | 117 | func hasSameState(i, j *lockboxv1.Condition) bool { 118 | return i.Type == j.Type && 119 | i.Status == j.Status && 120 | i.Reason == j.Reason && 121 | i.Severity == j.Severity && 122 | i.Message == j.Message 123 | } 124 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | import ( 7 | _ "sigs.k8s.io/controller-tools/cmd/controller-gen" 8 | ) 9 | --------------------------------------------------------------------------------