Skip to content

Commit 8e84485

Browse files
committed
Add SOPS age key auto-detection and improved error messages
- Auto-detect SOPS age key path when target is omitted - Bootstrap now detects source names containing 'sops' and 'age' - Automatically uses platform-specific default paths - Added helpful error messages when SOPS fails to decrypt - Suggests running 'comet bootstrap' or setting SOPS_AGE_KEY - Updated documentation to reflect optional target path - File permissions (0600) set correctly on creation
1 parent 070a117 commit 8e84485

6 files changed

Lines changed: 118 additions & 32 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.6.6] - 2025-10-23
11+
12+
### Added
13+
- **Auto-detect SOPS age key path** - Bootstrap `target` is now optional for SOPS age keys. If the source name contains "sops" and "age", it automatically uses the platform-specific default path
14+
- **Helpful SOPS error messages** - When SOPS fails to decrypt due to missing age keys, provide clear hint suggesting `comet bootstrap` or setting `SOPS_AGE_KEY`
15+
16+
### Changed
17+
- Improved bootstrap configuration - SOPS age key target path is now optional and auto-detected based on platform
18+
1019
## [0.6.5] - 2025-10-23
1120

1221
### Fixed
@@ -153,7 +162,8 @@ bootstrap:
153162
- Support for Terraform and OpenTofu
154163
- CLI commands: plan, apply, destroy, list, output, clean
155164

156-
[Unreleased]: https://github.com/moonwalker/comet/compare/v0.6.5...HEAD
165+
[Unreleased]: https://github.com/moonwalker/comet/compare/v0.6.6...HEAD
166+
[0.6.6]: https://github.com/moonwalker/comet/releases/tag/v0.6.6
157167
[0.6.5]: https://github.com/moonwalker/comet/releases/tag/v0.6.5
158168
[0.6.4]: https://github.com/moonwalker/comet/releases/tag/v0.6.4
159169
[0.6.3]: https://github.com/moonwalker/comet/releases/tag/v0.6.3

comet.yaml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ generate_backend: false
1616
# - name: sops-age-key
1717
# type: secret
1818
# source: op://vault/infrastructure/sops-age-key # Your 1Password path
19-
# target: ~/.config/sops/age/keys.txt # Automatically resolves to platform-specific path
19+
# # target is optional - automatically uses platform-specific SOPS age key location:
20+
# # macOS (default): ~/Library/Application Support/sops/age/keys.txt
21+
# # macOS (w/ XDG): $XDG_CONFIG_HOME/sops/age/keys.txt
22+
# # Linux: ~/.config/sops/age/keys.txt
23+
# # target: ~/.config/sops/age/keys.txt # Or specify a custom path
2024
# mode: "0600"
2125
#
22-
# # Note: For SOPS age keys, bootstrap automatically resolves the target path:
23-
# # - macOS without XDG_CONFIG_HOME: ~/Library/Application Support/sops/age/keys.txt
24-
# # - macOS with XDG_CONFIG_HOME: $XDG_CONFIG_HOME/sops/age/keys.txt
25-
# # - Linux: ~/.config/sops/age/keys.txt (or $XDG_CONFIG_HOME if set)
26+
# # Note: For SOPS age keys, if you omit 'target', bootstrap automatically
27+
# # detects it's a SOPS age key (based on source name containing 'sops' and 'age')
28+
# # and uses the correct platform-specific default path.
2629
#
2730
# # Optional: Check required tools
2831
# - name: check-tools

internal/bootstrap/runner.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,20 @@ func (r *Runner) runSecretStep(step *schema.BootstrapStep) error {
153153

154154
log.Debug("Secret fetched", "duration", duration)
155155

156-
// Expand target path
157-
targetPath := expandPath(step.Target)
156+
// Determine target path - use default for SOPS age keys if not specified
157+
targetPath := step.Target
158+
if targetPath == "" {
159+
// Auto-detect default path for common secret types
160+
if isSopsAgeKeySource(step.Source) {
161+
targetPath = getDefaultSopsAgePath()
162+
log.Debug("Using default SOPS age key path", "path", targetPath)
163+
} else {
164+
return fmt.Errorf("target path is required for secret type: %s", step.Source)
165+
}
166+
}
167+
168+
// Expand target path (handles ~, env vars, and platform-specific SOPS paths)
169+
targetPath = expandPath(targetPath)
158170

159171
// Create parent directory
160172
targetDir := filepath.Dir(targetPath)
@@ -279,6 +291,34 @@ func resolveSopsAgePath(path string) string {
279291
return filepath.Join(home, ".config", sopsAgeKeyPath)
280292
}
281293

294+
// isSopsAgeKeySource checks if the source is likely a SOPS age key
295+
func isSopsAgeKeySource(source string) bool {
296+
// Common patterns for SOPS age keys in secret managers
297+
lower := strings.ToLower(source)
298+
return strings.Contains(lower, "sops") &&
299+
(strings.Contains(lower, "age") || strings.Contains(lower, "key"))
300+
}
301+
302+
// getDefaultSopsAgePath returns the default SOPS age key path for the current platform
303+
func getDefaultSopsAgePath() string {
304+
// Check if XDG_CONFIG_HOME is set
305+
if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" {
306+
return filepath.Join(xdgConfigHome, "sops", "age", "keys.txt")
307+
}
308+
309+
home, err := os.UserHomeDir()
310+
if err != nil {
311+
// Fallback to a reasonable default
312+
return "~/.config/sops/age/keys.txt"
313+
}
314+
315+
if runtime.GOOS == "darwin" {
316+
return filepath.Join(home, "Library", "Application Support", "sops", "age", "keys.txt")
317+
}
318+
319+
return filepath.Join(home, ".config", "sops", "age", "keys.txt")
320+
}
321+
282322
// NeedsBootstrap checks if any bootstrap steps need to be run
283323
func NeedsBootstrap(config *schema.Config) bool {
284324
if len(config.Bootstrap) == 0 {

internal/secrets/sops.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@ func sopsData(ref string) (string, error) {
3232
ext := filepath.Ext(u.Path)
3333
b, err := decrypt.File(u.Path, ext)
3434
if err != nil {
35-
return "", err
35+
// Detect common SOPS errors and provide helpful messages
36+
errMsg := err.Error()
37+
if strings.Contains(errMsg, "no age identity") ||
38+
strings.Contains(errMsg, "0 successful groups required") ||
39+
strings.Contains(errMsg, "failed to get the data key") {
40+
return "", fmt.Errorf("failed to decrypt SOPS file: %w\n\nℹ️ Hint: Age key might be missing. Try running:\n comet bootstrap\n\nOr set the key manually:\n export SOPS_AGE_KEY=\"...\"", err)
41+
}
42+
return "", fmt.Errorf("failed to decrypt SOPS file: %w", err)
3643
}
3744

3845
if slices.Contains(yamlExts, ext) {

website/docs/guides/configuration.md

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ bootstrap:
6363
- name: sops-age-key
6464
type: secret
6565
source: op://vault/infrastructure/sops-age-key
66-
target: ~/.config/sops/age/keys.txt
66+
# target is optional - auto-detected for SOPS age keys
67+
# macOS: ~/Library/Application Support/sops/age/keys.txt
68+
# Linux: ~/.config/sops/age/keys.txt
6769
mode: "0600"
6870
```
6971

@@ -109,23 +111,30 @@ Fetch secrets from 1Password or SOPS and save to local files with proper permiss
109111

110112
```yaml
111113
bootstrap:
112-
# 1Password secret
114+
# 1Password secret - SOPS age key (target auto-detected)
113115
- name: sops-key
114116
type: secret
115-
source: op://vault/item/field # 1Password reference
116-
target: ~/.config/sops/age/keys.txt # Save location (~ expanded)
117-
mode: "0600" # File permissions (optional, default: 0600)
117+
source: op://vault/sops-age-key/private # Source contains "sops" and "age"
118+
# target is optional - automatically uses platform-specific path
119+
mode: "0600" # File permissions (optional, default: 0600)
118120
119-
# SOPS secret
121+
# Custom secret with explicit target
120122
- name: api-token
121123
type: secret
122-
source: sops://secrets.enc.yaml#/api/token # SOPS file reference
123-
target: ~/.secrets/api-token
124-
mode: "0400" # Read-only for extra security
125-
optional: true # Don't fail if source doesn't exist
124+
source: op://vault/api-token/credential
125+
target: ~/.secrets/api-token # Custom path (~ expanded)
126+
mode: "0400" # Read-only for extra security
127+
optional: true # Don't fail if source doesn't exist
128+
129+
# SOPS secret
130+
- name: db-password
131+
type: secret
132+
source: sops://secrets.enc.yaml#/database/password
133+
target: ~/.secrets/db-password
126134
```
127135

128136
**Features:**
137+
- **Auto-detect SOPS age key paths** - If source name contains "sops" and "age", target is optional and uses platform defaults
129138
- Automatically creates parent directories
130139
- Supports both `op://` (1Password) and `sops://` (SOPS) sources
131140
- Customizable file permissions (default: `0600`)
@@ -210,11 +219,11 @@ bootstrap:
210219
type: check
211220
command: op,sops,tofu
212221
213-
# 2. Fetch SOPS key from 1Password
222+
# 2. Fetch SOPS key from 1Password (target auto-detected)
214223
- name: sops-key
215224
type: secret
216225
source: op://vault/infrastructure/sops-age-key
217-
target: ~/.config/sops/age/keys.txt
226+
# target is optional for SOPS age keys
218227
mode: "0600"
219228
220229
# 3. Authenticate with cloud provider (optional)
@@ -287,7 +296,7 @@ bootstrap:
287296
- name: sops-key
288297
type: secret
289298
source: op://vault/sops/key
290-
target: ~/.config/sops/age/keys.txt
299+
# target is optional - auto-detected for SOPS age keys
291300
292301
- name: github-token
293302
type: secret
@@ -319,12 +328,12 @@ If you were using `op://` or `sops://` in the `env` section (removed in v0.6.0),
319328
env:
320329
SOPS_AGE_KEY: op://vault/key/private
321330
322-
# NEW (v0.6.0) - Fast after bootstrap
331+
# NEW (v0.6.0+) - Fast after bootstrap
323332
bootstrap:
324333
- name: sops-key
325334
type: secret
326335
source: op://vault/key/private
327-
target: ~/.config/sops/age/keys.txt
336+
# target optional for SOPS age keys (auto-detected)
328337
mode: "0600"
329338
```
330339

@@ -378,7 +387,7 @@ bootstrap:
378387
- name: sops-key
379388
type: secret
380389
source: op://vault/key/private
381-
target: ~/.config/sops/age/keys.txt
390+
# target optional (auto-detected for SOPS age keys)
382391
mode: "0600"
383392
```
384393

website/docs/guides/secrets-management.md

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ bootstrap:
7272
- name: sops-age-key
7373
type: secret
7474
source: op://vault/infrastructure/sops-age-key
75-
target: ~/.config/sops/age/keys.txt
75+
# target is optional - automatically uses platform-specific path
76+
# macOS: ~/Library/Application Support/sops/age/keys.txt
77+
# Linux: ~/.config/sops/age/keys.txt
7678
mode: "0600"
7779
```
7880
@@ -91,7 +93,9 @@ comet plan dev
9193

9294
:::tip Why Bootstrap?
9395

94-
SOPS needs the `SOPS_AGE_KEY` or `SOPS_AGE_KEY_FILE` environment variable to decrypt files. Instead of fetching this from 1Password on every command (4s overhead), bootstrap caches it to `~/.config/sops/age/keys.txt` once, making all subsequent commands fast.
96+
SOPS needs the `SOPS_AGE_KEY` or `SOPS_AGE_KEY_FILE` environment variable to decrypt files. Instead of fetching this from 1Password on every command (4s overhead), bootstrap caches it to the platform-specific default location once, making all subsequent commands fast.
97+
98+
The `target` path is optional - Comet automatically detects SOPS age keys (source containing "sops" and "age") and uses the correct platform-specific path where SOPS expects to find them.
9599

96100
:::
97101
age: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
@@ -343,7 +347,7 @@ bootstrap:
343347
- name: sops-key
344348
type: secret
345349
source: op://vault/sops-key/private
346-
target: ~/.config/sops/age/keys.txt
350+
# target is optional - auto-detected for SOPS age keys
347351
mode: "0600"
348352
```
349353
@@ -355,15 +359,28 @@ comet apply production
355359

356360
## Troubleshooting
357361

358-
### "Failed to get data key" Error
362+
### "Failed to get data key" / "0 successful groups required" Error
363+
364+
This usually means the age key is missing. Comet will suggest:
365+
366+
```
367+
ℹ️ Hint: Age key might be missing. Try running:
368+
comet bootstrap
369+
370+
Or set the key manually:
371+
export SOPS_AGE_KEY="..."
372+
```
359373

360-
Make sure your encryption key is available:
374+
Solutions:
361375

362376
```bash
363-
# For age
377+
# Option 1: Use bootstrap
378+
comet bootstrap
379+
380+
# Option 2: Set environment variable
364381
export SOPS_AGE_KEY_FILE=/path/to/key.txt
365382

366-
# For GPG
383+
# Option 3: For GPG
367384
gpg --list-secret-keys # Verify key is imported
368385
```
369386

0 commit comments

Comments
 (0)