Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
strategy:
matrix:
os: [macOS-latest, ubuntu-latest]
goversion: [1.19, "1.20"]
goversion: ["1.24"]
steps:
- name: Set up Go ${{matrix.goversion}} on ${{matrix.os}}
uses: actions/setup-go@v3
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
FROM golang:1.18.3-alpine as builder
FROM golang:1.24-alpine AS builder

RUN apk add --no-cache ca-certificates libc-dev git make gcc
RUN adduser -D pentagon
USER pentagon

# Enable go modules
ENV GO111MODULE on
ENV GO111MODULE=on

# The golang docker images configure GOPATH=/go
RUN mkdir -p /go/src/github.com/vimeo/pentagon /go/pkg/
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@
[![GoDoc](https://godoc.org/github.com/vimeo/pentagon?status.svg)](https://godoc.org/github.com/vimeo/pentagon) [![Go Report Card](https://goreportcard.com/badge/github.com/vimeo/pentagon)](https://goreportcard.com/report/github.com/vimeo/pentagon)

# Pentagon
Pentagon is a small application designed to run as a Kubernetes CronJob to periodically copy secrets stored in [Vault](https://www.vaultproject.io) into equivalent [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/), keeping them synchronized. Naturally, this should be used with care as "standard" Kubernetes Secrets are simply obfuscated as base64-encoded strings. However, one can and should use more secure methods of securing secrets including Google's [KMS](https://cloud.google.com/kubernetes-engine/docs/how-to/encrypting-secrets) and restricting roles and service accounts appropriately.

Use at your own risk...
Pentagon is a small application designed to run as a Kubernetes CronJob to periodically copy secrets stored in [Vault](https://www.vaultproject.io) or Google Secrets Manager into equivalent [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/), keeping them synchronized. Naturally, this should be used with care as "standard" Kubernetes Secrets are simply obfuscated as base64-encoded strings. However, one can and should use more secure methods of securing secrets including Google's [KMS](https://cloud.google.com/kubernetes-engine/docs/how-to/encrypting-secrets) and restricting roles and service accounts appropriately.

## Why not just query Vault?
That's a good question. If you have a highly-available Vault setup that is stable and performant and you're able to modify your applications to query Vault, that's a completely reasonable approach to take. If you don't have such a setup, Pentagon provides a way to cache things securely in Kubernetes secrets which can then be provided to applications without directly introducing a Vault dependency.

## Configuration
Pentagon requires a simple YAML configuration file, the path to which should be passed as the first and only argument to the application. It is recommended that you store this configuration in a [ConfigMap](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/) and reference it in the CronJob specification. A sample configuration follows:
Pentagon requires a YAML configuration file, the path to which should be passed as the first and only argument to the application. It is recommended that you store this configuration in a [ConfigMap](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/) and reference it in the CronJob specification. A sample configuration follows:

```yaml
vault:
Expand All @@ -28,6 +26,10 @@ mappings:
secretName: k8s-secretname
vaultEngineType: # optionally "kv" or "kv-v2" to override the defaultEngineType specified above
secretType: Opaque # optionally - default "Opaque" e.g.: "kubernetes.io/tls"
# mappings from google secrets manager paths to kubernetes secret names
- sourceType: gsm
gsmPath: projects/my-project/secrets/my-secret/versions/latest
secretName: my-secret
```

### Labels and Reconciliation
Expand Down
58 changes: 51 additions & 7 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,29 @@ import (
corev1 "k8s.io/api/core/v1"
)

// DefaultNamespace is the default kubernetes namespace.
const DefaultNamespace = "default"
const (
// DefaultNamespace is the default kubernetes namespace.
DefaultNamespace = "default"

// DefaultLabelValue is the default label value that will be applied to secrets
// created by pentagon.
const DefaultLabelValue = "default"
// DefaultLabelValue is the default label value that will be applied to secrets
// created by pentagon.
DefaultLabelValue = "default"

// VaultSourceType indicates a mapping sourced from Hashicorp Vault.
VaultSourceType = "vault"

// GSMSourceType indicates a mapping sourced from Google Secrets Manager.
GSMSourceType = "gsm"
)

// Config describes the configuration for vaultofsecrets
type Config struct {
// VaultURL is the URL used to connect to vault.
// Vault is the configuration used to connect to vault.
Vault VaultConfig `yaml:"vault"`

// GSM is the configuration used to connect to Google Secrets Manager.
GSM GSMConfig `yaml:"gsm"`

// Namespace is the k8s namespace that the secrets will be created in.
Namespace string `yaml:"namespace"`

Expand Down Expand Up @@ -51,6 +62,11 @@ func (c *Config) SetDefaults() {
// set all the underlying mapping engine types to their default
// if unspecified
for i, m := range c.Mappings {
// default to vault source type for backward compatibility
if m.SourceType == "" {
c.Mappings[i].SourceType = VaultSourceType
}

if m.VaultEngineType == "" {
c.Mappings[i].VaultEngineType = c.Vault.DefaultEngineType
}
Expand All @@ -66,6 +82,18 @@ func (c *Config) Validate() error {
return fmt.Errorf("no mappings provided")
}

validSourceTypes := map[string]struct{}{
"": {},
VaultSourceType: {},
GSMSourceType: {},
}

for _, m := range c.Mappings {
if _, ok := validSourceTypes[m.SourceType]; !ok {
return fmt.Errorf("invalid source type: %+v", m.SourceType)
}
}

return nil
}

Expand Down Expand Up @@ -97,11 +125,27 @@ type VaultConfig struct {
TLSConfig *api.TLSConfig `yaml:"tls"` // for other vault TLS options
}

// GSMConfig is the Google Secrets Manager configuration.
type GSMConfig struct {
// Will add fields as needed.
}

// Mapping is a single mapping for a vault secret to a k8s secret.
type Mapping struct {
// VaultPath is the path to the vault secret.
// SourceType is the source of a secret: Vault or GSM. Defaults to Vault.
SourceType string `yaml:"sourceType"`

// VaultPath is the path to a vault secret.
VaultPath string `yaml:"vaultPath"`

// GSMPath is the full-qualified path of a GSM secret, including its name and version.
// For example:
// - projects/*/secrets/*/versions/*
// - projects/*/secrets/*/versions/latest
// - projects/*/locations/*/secrets/*/versions/*
// - projects/*/locations/*/secrets/*/versions/latest
GSMPath string `yaml:"gsmPath"`

// SecretName is the name of the k8s secret that the vault contents should
// be written to. Note that this must be a DNS-1123-compatible name and
// match the regex [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
Expand Down
27 changes: 27 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ func TestSetDefaults(t *testing.T) {
}

for _, m := range c.Mappings {
if m.SourceType != VaultSourceType {
t.Fatalf("source type should have defaulted to vault: %+v", m)
}
if m.VaultEngineType == "" {
t.Fatalf("empty vault engine type for mapping: %+v", m)
}
Expand Down Expand Up @@ -91,3 +94,27 @@ func TestValidate(t *testing.T) {
t.Fatalf("configuration should have been valid: %s", err)
}
}

func TestValidSourceTypes(t *testing.T) {
c := &Config{
Mappings: []Mapping{
{SourceType: ""},
{SourceType: VaultSourceType},
{SourceType: GSMSourceType},
},
}
if err := c.Validate(); err != nil {
t.Fatalf("mappings should have been valid: %s", err)
}
}

func TestInvalidSourceType(t *testing.T) {
c := &Config{
Mappings: []Mapping{
{SourceType: "foo"},
},
}
if err := c.Validate(); err == nil {
t.Fatalf("failed to detect invalid mapping source type")
}
}
53 changes: 37 additions & 16 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
module github.com/vimeo/pentagon

go 1.18
go 1.23.0

toolchain go1.24.2

require (
cloud.google.com/go/compute/metadata v0.2.3
cloud.google.com/go/compute/metadata v0.7.0
cloud.google.com/go/secretmanager v1.15.0
github.com/googleapis/gax-go/v2 v2.14.2
github.com/hashicorp/vault/api v1.9.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.26.3
Expand All @@ -12,22 +16,28 @@ require (
)

require (
cloud.google.com/go/compute v1.19.0 // indirect
cloud.google.com/go/auth v0.16.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/iam v1.5.2 // indirect
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.10.2 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic v0.6.9 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
Expand All @@ -50,15 +60,26 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/oauth2 v0.6.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/api v0.237.0 // indirect
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
Loading