Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# go-cloudsecrets

Go package for hydrating config secrets from Cloud secret providers:
- [x] `gcp` — GCP Secret Manager
- [x] `env` — Environment variables (configurable prefix)
- [x] [`gcp`](./gcp/README.md) — GCP Secret Manager
- [x] [`env`](./env/README.md) — Environment variables (configurable prefix)
- [x] [`onepassword`](./onepassword/README.md) — 1Password Secrets (service account)
- [x] `nosecrets` — No provider (errors out on any `$SECRET:` value)

```go
Expand Down
7 changes: 5 additions & 2 deletions _examples/Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
SHELL = bash -o pipefail

run:
go run cmd/main.go
run-gcp:
go run cmd/gcp/main.go

run-onepassword:
go run cmd/onepassword/main.go
File renamed without changes.
41 changes: 41 additions & 0 deletions _examples/cmd/onepassword/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package main

import (
"context"
"fmt"
"log"

"github.com/kr/pretty"

"github.com/0xsequence/go-cloudsecrets"
"github.com/0xsequence/go-cloudsecrets/_examples/config"
"github.com/0xsequence/go-cloudsecrets/onepassword"
)

func main() {
// Adjust the op:// references to point at items in a vault your 1Password
// CLI session can read. Authenticate via biometric desktop integration,
// `eval "$(op signin)"`, or OP_SERVICE_ACCOUNT_TOKEN before running.
var cfg = &config.Config{
DB: &config.DB{
Database: "db_name",
Host: "localhost:5432",
Username: "$SECRET:op://cloudsecrets-test/db/username",
Password: "$SECRET:op://cloudsecrets-test/db/password",
},
}

ctx := context.Background()

provider, err := onepassword.NewSecretsProvider(ctx)
if err != nil {
log.Fatalf("failed to create secrets provider: %v", err)
}

err = cloudsecrets.Hydrate(ctx, provider, cfg)
if err != nil {
log.Fatalf("failed to hydrate config secrets: %v", err)
}

fmt.Printf("%# v", pretty.Formatter(cfg))
}
48 changes: 48 additions & 0 deletions env/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# env

A `SecretsProvider` backed by environment variables. Intended for local development and tests, not production — process env is visible to anything that can read `/proc/<pid>/environ` on Linux or attach a debugger.

## Usage

```go
import (
"github.com/0xsequence/go-cloudsecrets"
"github.com/0xsequence/go-cloudsecrets/env"
)

type Config struct {
DBPassword string
}

cfg := Config{
DBPassword: "$SECRET:dbPassword",
}

func main() {
ctx := context.Background()

// The prefix is prepended to each secret ID before reading os.Getenv.
// With prefix "MYAPP_SECRET_", "$SECRET:dbPassword" reads MYAPP_SECRET_dbPassword.
provider := env.NewSecretsProvider("MYAPP_SECRET_")

if err := cloudsecrets.Hydrate(ctx, provider, &cfg); err != nil {
log.Fatalf("failed to hydrate config secrets: %v", err)
}
}
```

Then run with the matching env vars:

```bash
MYAPP_SECRET_dbPassword=hunter2 ./myapp
```

## Reference format

Secret IDs are appended directly to the configured prefix. Use whatever naming convention you like — the env var is `<prefix><secretId>`.

## Caveats

- An empty value (`MYAPP_SECRET_dbPassword=`) is treated as "not set" and returns an error.
- No type coercion — values are returned as-is to the hydrator.
- Don't use this in production. Use `gcp` or `onepassword` instead.
88 changes: 88 additions & 0 deletions gcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# gcp

A `SecretsProvider` backed by [GCP Secret Manager](https://cloud.google.com/secret-manager).

## Setup

### 1. Enable the Secret Manager API

```bash
gcloud services enable secretmanager.googleapis.com
```

### 2. Create secrets

```bash
echo -n "hunter2" | gcloud secrets create dbPassword --data-file=-
```

Secret IDs are arbitrary — use whatever identifier you want callers to reference (e.g. `dbPassword`, `stripe_key`).

### 3. Authenticate

The provider uses [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials).

**On GCE / GKE / Cloud Run / Cloud Functions:** ADC works automatically via the metadata server. No setup required beyond granting the workload identity the right role (next step).

**Locally:**

```bash
gcloud auth application-default login
```

### 4. Grant `secretAccessor` IAM

The identity running the workload (or your local user) needs `roles/secretmanager.secretAccessor` on the project — or, more narrowly, on individual secrets.

```bash
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:my-service@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
```

### 5. Project resolution

The provider needs the project **number** (not project ID) for the Secret Manager API. It resolves this automatically:

- On GCE / GKE / etc.: from the metadata server.
- Locally: from `GOOGLE_CLOUD_PROJECT` env var if set, otherwise from `gcloud config get-value project`.

## Usage

```go
import (
"github.com/0xsequence/go-cloudsecrets"
"github.com/0xsequence/go-cloudsecrets/gcp"
)

type Config struct {
DBPassword string
}

cfg := Config{
DBPassword: "$SECRET:dbPassword",
}

func main() {
ctx := context.Background()

provider, err := gcp.NewSecretsProvider(ctx)
if err != nil {
log.Fatalf("failed to create secrets provider: %v", err)
}
defer provider.Close()

if err := cloudsecrets.Hydrate(ctx, provider, &cfg); err != nil {
log.Fatalf("failed to hydrate config secrets: %v", err)
}
}
```

## Reference format

Secret IDs are passed verbatim as the GCP secret name. The provider always reads version `latest`.

## Caveats

- Per-call timeout is 10 seconds.
- Always call `provider.Close()` to release the underlying gRPC connection.
40 changes: 20 additions & 20 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
module github.com/0xsequence/go-cloudsecrets

go 1.23.0
go 1.24.0

require (
cloud.google.com/go/compute/metadata v0.6.0
cloud.google.com/go/compute/metadata v0.7.0
cloud.google.com/go/secretmanager v1.11.5
github.com/google/go-cmp v0.6.0
github.com/stretchr/testify v1.8.4
golang.org/x/sync v0.6.0
github.com/google/go-cmp v0.7.0
github.com/stretchr/testify v1.11.1
golang.org/x/sync v0.16.0
)

require (
cloud.google.com/go/iam v1.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
go.opentelemetry.io/otel v1.22.0 // indirect
go.opentelemetry.io/otel/metric v1.22.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/oauth2 v0.16.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.14.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/api v0.160.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect
google.golang.org/grpc v1.61.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading
Loading