Skip to content

Commit 6990b0d

Browse files
authored
Merge pull request #113 from udx/env-as-secret
Enhanced Secret Fetching with Environment Variable Support
2 parents e29ba8b + 52045e9 commit 6990b0d

File tree

5 files changed

+124
-54
lines changed

5 files changed

+124
-54
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ UDX Worker is a containerized solution that simplifies DevSecOps by providing:
1212

1313
- 🔒 **Secure Environment**: Built on zero-trust principles
1414
- 🤖 **Automation Support**: Streamlined task execution
15-
- 🔑 **Secret Management**: Secure handling of sensitive data
15+
- 🔑 **Secret Management**: Automatic detection and resolution from multiple providers
1616
- 📦 **12-Factor Compliance**: Modern application practices
17-
- ♾️ **CI/CD Ready**: Seamless pipeline integration
17+
- ♾️ **CI/CD Ready**: Seamless pipeline integration with environment-based overrides
1818

1919
## 🏃 Quick Start
2020

docs/config.md

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -110,24 +110,88 @@ config:
110110

111111
The worker will automatically detect secret references (format: `provider/vault/secret`) in environment variables and resolve them at runtime.
112112

113+
## Deployment Environment Variables
114+
115+
Environment variables can also be set at the deployment level (e.g., Kubernetes, Docker Compose) and will **take priority** over `worker.yaml` configuration.
116+
117+
### Priority Order
118+
119+
1. **Deployment environment variables** (highest priority)
120+
2. `worker.yaml` config.secrets
121+
3. `worker.yaml` config.env
122+
123+
### Example: Environment Override
124+
125+
```yaml
126+
# worker.yaml (production defaults)
127+
config:
128+
secrets:
129+
ES_PASSWORD: "gcp/prod-project/es-password"
130+
```
131+
132+
```yaml
133+
# Kubernetes deployment (staging override)
134+
spec:
135+
containers:
136+
- name: worker
137+
env:
138+
- name: ES_PASSWORD
139+
value: "gcp/staging-project/es-password"
140+
```
141+
142+
**Result**: The staging secret reference from Kubernetes will be used, not the production one from `worker.yaml`.
143+
144+
### Use Cases
145+
146+
- **Environment-specific overrides**: Different secrets per environment (dev/staging/prod)
147+
- **Sensitive values**: Keep secrets out of config files entirely
148+
- **Dynamic configuration**: Runtime values that change per deployment
149+
- **Testing**: Override config values without modifying files
150+
151+
### Behavior
152+
153+
- Deployment env vars with secret references are automatically detected and resolved
154+
- Deployment env vars with static values are used as-is
155+
- If a deployment env var exists, the corresponding `worker.yaml` entry is skipped
156+
113157
## Best Practices
114158

115159
1. **Secret Management**
116160

117161
- Never store sensitive values as plain text
118-
- Use either `config.secrets` section OR secret references in `config.env`
119-
- Both methods support the same provider format: `provider/vault/secret`
120-
- Choose based on your preference:
162+
- Use secret references in any of these locations:
121163
- `config.secrets`: Explicit separation of secrets
122-
- `config.env` with references: Unified configuration
123-
124-
2. **Environment Variables**
125-
126-
- Use `env` for non-sensitive configuration OR secret references
127-
- Keep values consistent across environments
164+
- `config.env`: Unified configuration with secret references
165+
- Deployment env vars: Runtime overrides (highest priority)
166+
- All methods support the same provider format: `provider/vault/secret`
167+
168+
2. **Environment-Specific Configuration**
169+
170+
- Use `worker.yaml` for shared/default configuration
171+
- Use deployment env vars for environment-specific overrides
172+
- Example pattern:
173+
```yaml
174+
# worker.yaml: production defaults
175+
config:
176+
secrets:
177+
DB_PASSWORD: "gcp/prod/db-pass"
178+
179+
# K8s staging: override with deployment env
180+
env:
181+
- name: DB_PASSWORD
182+
value: "gcp/staging/db-pass"
183+
```
184+
185+
3. **Environment Variables**
186+
187+
- Use `config.env` for non-sensitive configuration OR secret references
188+
- Use deployment env vars to override per environment
128189
- Document any required variables
190+
- Remember: deployment env vars always win
191+
192+
4. **File Handling**
129193

130-
3. **File Handling**
131-
- Keep configuration in version control (without sensitive data)
132-
- Use different files for different environments
194+
- Keep `worker.yaml` in version control (without sensitive data)
195+
- Use secret references instead of plain text values
133196
- Validate configuration before deployment
197+
- Use deployment env vars for truly sensitive overrides

lib/env_handler.sh

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,13 @@ generate_env_file() {
5151
done < <(echo "$config" | yq eval '.config.env | to_entries | .[] | "export " + .key + "=\"" + .value + "\""' -)
5252
}
5353

54-
# Append resolved secrets to environment file
55-
append_resolved_secrets() {
54+
# Internal function to resolve and append secrets
55+
# Parameters:
56+
# $1 - secrets_json: JSON object of secrets to resolve
57+
# $2 - respect_deployment_env: if "true", skip secrets that exist in deployment environment
58+
_resolve_and_append_secrets() {
5659
local secrets_json="$1"
60+
local respect_deployment_env="${2:-false}"
5761
local has_failures=false
5862

5963
if [ -z "$secrets_json" ]; then
@@ -78,6 +82,18 @@ append_resolved_secrets() {
7882
local name value
7983
name=$(echo "$secret" | jq -r '.key')
8084

85+
# Check if variable exists in deployment environment (only if respect_deployment_env is true)
86+
if [[ "$respect_deployment_env" == "true" ]] && printenv "$name" > /dev/null 2>&1; then
87+
local deploy_value
88+
deploy_value="$(printenv "$name")"
89+
if [[ "$deploy_value" =~ ^(${SUPPORTED_SECRET_PROVIDERS})/.+/.+ ]]; then
90+
log_info "Environment" "Skipping [$name] from config.secrets - will be resolved from deployment environment secret reference"
91+
else
92+
log_info "Environment" "Skipping [$name] from config.secrets - using deployment environment static value"
93+
fi
94+
continue
95+
fi
96+
8197
# Create config JSON for resolve_secret_by_name
8298
local config_json
8399
config_json="{ \"config\": { \"secrets\": { \"$name\": $(echo "$secret" | jq '.value') } } }"
@@ -105,6 +121,18 @@ append_resolved_secrets() {
105121
rm -f "$temp_file"
106122
}
107123

124+
# Resolve secrets from worker.yaml config.secrets section
125+
# Respects deployment environment - skips secrets that exist in deployment env
126+
append_resolved_secrets() {
127+
_resolve_and_append_secrets "$1" "true"
128+
}
129+
130+
# Resolve secrets detected in environment variables
131+
# Always resolves - deployment env vars take precedence by being processed here
132+
resolve_env_var_secrets() {
133+
_resolve_and_append_secrets "$1" "false"
134+
}
135+
108136
# Load environment variables and secrets
109137
load_environment() {
110138
if [ -f "$WORKER_ENV_FILE" ]; then

lib/secrets.sh

Lines changed: 10 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,7 @@ is_secret_reference() {
161161
local value="$1"
162162

163163
# Check if value matches pattern: provider/vault/secret
164-
# Supported providers: gcp, azure, aws, bitwarden
165-
if [[ "$value" =~ ^(gcp|azure|aws|bitwarden)/.+/.+ ]]; then
164+
if [[ "$value" =~ ^(${SUPPORTED_SECRET_PROVIDERS})/.+/.+ ]]; then
166165
return 0
167166
fi
168167
return 1
@@ -198,7 +197,6 @@ should_skip_variable() {
198197

199198
# Function to fetch secrets from environment variables
200199
fetch_secrets_from_env_vars() {
201-
local processed_vars=()
202200
local -a secret_keys=()
203201
local -a secret_values=()
204202

@@ -219,40 +217,9 @@ fetch_secrets_from_env_vars() {
219217
secret_values+=("$var_value")
220218
}
221219

222-
# 1. Process environment variables from worker.yaml (in WORKER_ENV_FILE)
223-
if [[ -f "$WORKER_ENV_FILE" ]]; then
224-
while IFS= read -r line; do
225-
# Skip comments and empty lines
226-
[[ "$line" =~ ^#.*$ || -z "$line" ]] && continue
227-
228-
# Extract variable name and value
229-
if [[ "$line" =~ ^export[[:space:]]+([^=]+)=\"([^\"]*)\" ]]; then
230-
local var_name="${BASH_REMATCH[1]}"
231-
local var_value="${BASH_REMATCH[2]}"
232-
233-
# Track that we've processed this variable
234-
processed_vars+=("$var_name")
235-
236-
# Collect if it's a secret reference
237-
collect_secret "$var_name" "$var_value"
238-
fi
239-
done < "$WORKER_ENV_FILE"
240-
fi
241-
242-
# 2. Process deployment environment variables (from container environment)
220+
# Scan all environment variables for secret references
221+
# This includes both worker.yaml config.env and deployment env vars
243222
while IFS='=' read -r key value; do
244-
# Skip if already processed from worker config
245-
local already_processed=false
246-
for processed_var in "${processed_vars[@]}"; do
247-
if [[ "$key" == "$processed_var" ]]; then
248-
already_processed=true
249-
break
250-
fi
251-
done
252-
if [[ "$already_processed" == "true" ]]; then
253-
continue
254-
fi
255-
256223
# Skip system and worker internal variables
257224
if should_skip_variable "$key"; then
258225
continue
@@ -264,7 +231,10 @@ fetch_secrets_from_env_vars() {
264231

265232
# Build JSON from collected secrets in a single jq invocation
266233
local secrets_json="{}"
234+
local secret_count=0
267235
if [[ ${#secret_keys[@]} -gt 0 ]]; then
236+
secret_count=${#secret_keys[@]}
237+
268238
# Build jq arguments for all key-value pairs
269239
local jq_args=()
270240
for i in "${!secret_keys[@]}"; do
@@ -287,14 +257,16 @@ fetch_secrets_from_env_vars() {
287257
return 0
288258
fi
289259

260+
log_info "Found $secret_count secret reference(s) in environment variables"
261+
290262
# Validate JSON (should always be valid since jq built it)
291263
if ! echo "$secrets_json" | jq empty > /dev/null 2>&1; then
292264
log_error "Secrets" "Invalid JSON format for collected secrets"
293265
return 1
294266
fi
295267

296-
# Use existing append_resolved_secrets function to resolve and append
297-
if ! append_resolved_secrets "$secrets_json"; then
268+
# Resolve secrets from environment variables (always resolves, no skipping)
269+
if ! resolve_env_var_secrets "$secrets_json"; then
298270
log_error "Secrets" "Failed to resolve secrets from environment variables"
299271
return 1
300272
fi

lib/utils.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
#!/bin/bash
22

3+
# Supported secret providers (used for secret reference detection)
4+
# Only declare if not already defined (prevents errors when sourced multiple times)
5+
if [[ -z "${SUPPORTED_SECRET_PROVIDERS+x}" ]]; then
6+
readonly SUPPORTED_SECRET_PROVIDERS="gcp|azure|aws|bitwarden"
7+
fi
8+
39
# Function to resolve placeholders with environment variables
410
resolve_env_vars() {
511
local value="$1"

0 commit comments

Comments
 (0)