Skip to content

Commit 1ececdc

Browse files
authored
Merge pull request #91 from diggerhq/secrets-setup
Secrets vault
2 parents fab434b + 9e8a3d2 commit 1ececdc

7 files changed

Lines changed: 243 additions & 0 deletions

File tree

cmd/server/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ import (
2727
)
2828

2929
func main() {
30+
// Load secrets from Azure Key Vault if configured (before config.Load reads env vars).
31+
if err := config.LoadSecretsFromKeyVault(); err != nil {
32+
log.Fatalf("failed to load secrets from Key Vault: %v", err)
33+
}
34+
3035
cfg, err := config.Load()
3136
if err != nil {
3237
log.Fatalf("failed to load config: %v", err)

cmd/worker/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ import (
3030
var AgentVersion = "dev"
3131

3232
func main() {
33+
// Load secrets from Azure Key Vault if configured (before config.Load reads env vars).
34+
if err := config.LoadSecretsFromKeyVault(); err != nil {
35+
log.Fatalf("failed to load secrets from Key Vault: %v", err)
36+
}
37+
3338
cfg, err := config.Load()
3439
if err != nil {
3540
log.Fatalf("failed to load config: %v", err)

deploy/server.env.example

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# OpenComputer Control Plane — Environment Configuration
2+
#
3+
# Place at /etc/opensandbox/server.env on the control plane VM.
4+
# Secrets are loaded from Secrets Service at startup when SECRETS_VAULT_NAME is set.
5+
# Non-secret config values stay in this file.
6+
7+
# ── Secrets Service ──────────────────────────────────────────────
8+
# Set this to enable secrets loading from secrets service.
9+
# When set, all values marked [KEY VAULT] below are fetched automatically.
10+
# Remove those values from this file — they're managed in the vault.
11+
SECRETS_VAULT_NAME=opencomputer-prod-kv
12+
13+
# ── Server Config (not secret — stays in this file) ─────────────
14+
OPENSANDBOX_MODE=server
15+
OPENSANDBOX_PORT=8080
16+
OPENSANDBOX_REGION=eastus2
17+
OPENSANDBOX_SANDBOX_DOMAIN=workers.opencomputer.dev
18+
OPENSANDBOX_CONTROLPLANE_IP=10.200.1.4
19+
OPENSANDBOX_HTTP_ADDR=https://app.opencomputer.dev
20+
OPENSANDBOX_MAX_WORKERS=4
21+
OPENSANDBOX_MIN_WORKERS=1
22+
23+
# WorkOS OAuth (non-secret config)
24+
WORKOS_REDIRECT_URI=https://app.opencomputer.dev/auth/callback
25+
WORKOS_COOKIE_DOMAIN=opencomputer.dev
26+
27+
# Stripe (non-secret config)
28+
STRIPE_SUCCESS_URL=https://app.opencomputer.dev/billing?success=true
29+
STRIPE_CANCEL_URL=https://app.opencomputer.dev/billing?cancelled=true
30+
31+
# ── Secrets (managed in secrets service) ───────────────────────────────
32+
# These are loaded from Secrets Service at startup.
33+
# Only set them here to override secrets service (e.g., local development).
34+
#
35+
# secrets service secret name → Environment variable
36+
# ─────────────────────────────────────────────────────────
37+
# server-database-url → OPENSANDBOX_DATABASE_URL
38+
# server-redis-url → OPENSANDBOX_REDIS_URL
39+
# server-jwt-secret → OPENSANDBOX_JWT_SECRET
40+
# server-api-key → OPENSANDBOX_API_KEY
41+
# server-secret-encryption-key → OPENSANDBOX_SECRET_ENCRYPTION_KEY
42+
# server-workos-api-key → WORKOS_API_KEY
43+
# server-workos-client-id → WORKOS_CLIENT_ID
44+
# server-cf-api-token → OPENSANDBOX_CF_API_TOKEN
45+
# server-cf-zone-id → OPENSANDBOX_CF_ZONE_ID
46+
# server-stripe-secret-key → STRIPE_SECRET_KEY
47+
# server-stripe-webhook-secret → STRIPE_WEBHOOK_SECRET

deploy/worker.env.example

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# OpenComputer Worker — Environment Configuration
2+
#
3+
# Place at /etc/opensandbox/worker.env on each worker VM.
4+
# Secrets are loaded from Secrets Service at startup when SECRETS_VAULT_NAME is set.
5+
# Non-secret config values stay in this file.
6+
7+
# ── Secrets Service ──────────────────────────────────────────────
8+
# Set this to enable secrets loading from secrets service.
9+
# When set, all values marked [KEY VAULT] below are fetched automatically.
10+
# Remove those values from this file — they're managed in the vault.
11+
SECRETS_VAULT_NAME=opencomputer-prod-kv
12+
13+
# ── Worker Config (not secret — stays in this file) ──────────────
14+
OPENSANDBOX_MODE=worker
15+
OPENSANDBOX_VM_BACKEND=qemu
16+
OPENSANDBOX_QEMU_BIN=qemu-system-x86_64
17+
OPENSANDBOX_DATA_DIR=/data/sandboxes
18+
OPENSANDBOX_KERNEL_PATH=/opt/opensandbox/vmlinux
19+
OPENSANDBOX_IMAGES_DIR=/data/firecracker/images
20+
OPENSANDBOX_GRPC_ADVERTISE=10.200.1.5:9090
21+
OPENSANDBOX_HTTP_ADDR=http://10.200.1.5:8081
22+
OPENSANDBOX_WORKER_ID=w-oc-eastus2-1
23+
OPENSANDBOX_MACHINE_ID=oc-worker-1
24+
OPENSANDBOX_REGION=eastus2
25+
OPENSANDBOX_PORT=8081
26+
OPENSANDBOX_MAX_CAPACITY=250
27+
OPENSANDBOX_SANDBOX_DOMAIN=workers.opencomputer.dev
28+
29+
# VM defaults
30+
OPENSANDBOX_DEFAULT_SANDBOX_MEMORY_MB=1024
31+
OPENSANDBOX_DEFAULT_SANDBOX_CPUS=2
32+
OPENSANDBOX_DEFAULT_SANDBOX_DISK_MB=20480
33+
34+
# S3/Blob storage (non-secret config)
35+
OPENSANDBOX_S3_BUCKET=checkpoints
36+
OPENSANDBOX_S3_REGION=eastus2
37+
OPENSANDBOX_S3_ENDPOINT=https://occkpt3ccf3c31.blob.core.windows.net
38+
39+
# ── Secrets (managed in secrets service) ───────────────────────────────
40+
# These are loaded from Secrets Service at startup.
41+
# Only set them here to override secrets service (e.g., local development).
42+
#
43+
# secrets service secret name → Environment variable
44+
# ─────────────────────────────────────────────────────────
45+
# worker-jwt-secret → OPENSANDBOX_JWT_SECRET
46+
# worker-database-url → OPENSANDBOX_DATABASE_URL
47+
# worker-redis-url → OPENSANDBOX_REDIS_URL
48+
# worker-s3-access-key → OPENSANDBOX_S3_ACCESS_KEY_ID
49+
# worker-s3-secret-key → OPENSANDBOX_S3_SECRET_ACCESS_KEY

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
88
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0
99
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0
10+
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0
1011
github.com/aws/aws-sdk-go-v2 v1.41.2
1112
github.com/aws/aws-sdk-go-v2/config v1.32.10
1213
github.com/aws/aws-sdk-go-v2/credentials v1.19.10
@@ -35,6 +36,7 @@ require (
3536

3637
require (
3738
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
39+
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect
3840
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
3941
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
4042
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0
1414
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0/go.mod h1:243D9iHbcQXoFUtgHJwL7gl2zx1aDuDMjvBZVGr2uW0=
1515
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8=
1616
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0/go.mod h1:s1tW/At+xHqjNFvWU4G0c0Qv33KOhvbGNj0RCTQDV8s=
17+
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 h1:/g8S6wk65vfC6m3FIxJ+i5QDyN9JWwXI8Hb0Img10hU=
18+
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0/go.mod h1:gpl+q95AzZlKVI3xSoseF9QPrypk0hQqBiJYeB/cR/I=
19+
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=
20+
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=
1721
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
1822
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
1923
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=

internal/config/keyvault.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Package config provides configuration loading from Azure Key Vault.
2+
//
3+
// If SECRETS_VAULT_NAME is set, LoadSecretsFromKeyVault fetches all secrets
4+
// from the vault and maps them to environment variables. The mapping is:
5+
//
6+
// Key Vault secret name → Environment variable
7+
// server-database-url → OPENSANDBOX_DATABASE_URL
8+
// server-jwt-secret → OPENSANDBOX_JWT_SECRET
9+
// worker-s3-secret-key → OPENSANDBOX_S3_SECRET_ACCESS_KEY
10+
// ...etc
11+
//
12+
// Secrets already set in the environment are NOT overwritten — env vars take
13+
// precedence over Key Vault. This allows local overrides for development.
14+
//
15+
// Authentication uses Azure Default Credential (Managed Identity on VMs,
16+
// CLI credentials locally). No explicit credentials needed.
17+
package config
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"log"
23+
"os"
24+
"strings"
25+
"time"
26+
27+
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
28+
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets"
29+
)
30+
31+
// secretMapping maps Key Vault secret names to environment variable names.
32+
// Only secrets in this map are loaded — unknown secrets in the vault are ignored.
33+
var secretMapping = map[string]string{
34+
// Server secrets
35+
"server-database-url": "OPENSANDBOX_DATABASE_URL",
36+
"server-redis-url": "OPENSANDBOX_REDIS_URL",
37+
"server-jwt-secret": "OPENSANDBOX_JWT_SECRET",
38+
"server-api-key": "OPENSANDBOX_API_KEY",
39+
"server-secret-encryption-key": "OPENSANDBOX_SECRET_ENCRYPTION_KEY",
40+
"server-workos-api-key": "WORKOS_API_KEY",
41+
"server-workos-client-id": "WORKOS_CLIENT_ID",
42+
"server-cf-api-token": "OPENSANDBOX_CF_API_TOKEN",
43+
"server-cf-zone-id": "OPENSANDBOX_CF_ZONE_ID",
44+
"server-stripe-secret-key": "STRIPE_SECRET_KEY",
45+
"server-stripe-webhook-secret": "STRIPE_WEBHOOK_SECRET",
46+
47+
// Worker secrets
48+
"worker-jwt-secret": "OPENSANDBOX_JWT_SECRET",
49+
"worker-database-url": "OPENSANDBOX_DATABASE_URL",
50+
"worker-redis-url": "OPENSANDBOX_REDIS_URL",
51+
"worker-s3-access-key": "OPENSANDBOX_S3_ACCESS_KEY_ID",
52+
"worker-s3-secret-key": "OPENSANDBOX_S3_SECRET_ACCESS_KEY",
53+
54+
// Shared
55+
"pg-password": "OPENSANDBOX_PG_PASSWORD",
56+
}
57+
58+
// LoadSecretsFromKeyVault fetches secrets from Azure Key Vault and sets them
59+
// as environment variables. Only loads secrets relevant to the current mode
60+
// (server or worker), determined by the secret name prefix.
61+
//
62+
// Skips secrets that are already set in the environment.
63+
// Does nothing if SECRETS_VAULT_NAME is not set.
64+
func LoadSecretsFromKeyVault() error {
65+
vaultName := os.Getenv("SECRETS_VAULT_NAME")
66+
if vaultName == "" {
67+
return nil // Key Vault not configured — use env file as-is
68+
}
69+
70+
vaultURL := fmt.Sprintf("https://%s.vault.azure.net/", vaultName)
71+
mode := os.Getenv("OPENSANDBOX_MODE") // "server" or "worker"
72+
73+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
74+
defer cancel()
75+
76+
cred, err := azidentity.NewDefaultAzureCredential(nil)
77+
if err != nil {
78+
return fmt.Errorf("keyvault: azure credential: %w", err)
79+
}
80+
81+
client, err := azsecrets.NewClient(vaultURL, cred, nil)
82+
if err != nil {
83+
return fmt.Errorf("keyvault: client: %w", err)
84+
}
85+
86+
loaded := 0
87+
skipped := 0
88+
89+
pager := client.NewListSecretPropertiesPager(nil)
90+
for pager.More() {
91+
page, err := pager.NextPage(ctx)
92+
if err != nil {
93+
return fmt.Errorf("keyvault: list secrets: %w", err)
94+
}
95+
96+
for _, prop := range page.Value {
97+
name := prop.ID.Name()
98+
envVar, mapped := secretMapping[name]
99+
if !mapped {
100+
continue
101+
}
102+
103+
// Only load secrets matching the current mode (or shared secrets)
104+
if mode != "" && !strings.HasPrefix(name, mode+"-") && !strings.HasPrefix(name, "pg-") {
105+
continue
106+
}
107+
108+
// Don't overwrite existing env vars — local config takes precedence
109+
if os.Getenv(envVar) != "" {
110+
skipped++
111+
continue
112+
}
113+
114+
// Fetch the secret value
115+
resp, err := client.GetSecret(ctx, name, "", nil)
116+
if err != nil {
117+
log.Printf("keyvault: failed to get secret %s: %v (skipping)", name, err)
118+
continue
119+
}
120+
if resp.Value == nil {
121+
continue
122+
}
123+
124+
os.Setenv(envVar, *resp.Value)
125+
loaded++
126+
}
127+
}
128+
129+
log.Printf("keyvault: loaded %d secrets from %s (%d skipped, already set)", loaded, vaultName, skipped)
130+
return nil
131+
}

0 commit comments

Comments
 (0)