Skip to content

Commit ace1c6a

Browse files
committed
integrate secretspec.dev
1 parent 0b09610 commit ace1c6a

File tree

16 files changed

+752
-8
lines changed

16 files changed

+752
-8
lines changed

Cargo.lock

Lines changed: 361 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

devenv.nix

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
pkgs.cargo-outdated # Find outdated crates
2828
pkgs.cargo-machete # Find unused crates
2929
pkgs.cargo-edit # Adds the set-version command
30-
pkgs.protobuf
30+
pkgs.protobuf # snix
31+
pkgs.dbus # secretspec
3132
];
3233

3334
languages.nix.enable = true;

devenv/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ shell-escape.workspace = true
5353
rmcp.workspace = true
5454
rmcp-macros.workspace = true
5555
async-trait.workspace = true
56+
secretspec = "0.2.0"
5657

5758
# Optional snix dependencies
5859
snix-eval = { git = "https://github.com/cachix/snix", optional = true }

devenv/src/config.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,20 @@ pub struct Config {
158158
pub impure: bool,
159159
#[serde(default, skip_serializing_if = "is_default")]
160160
pub backend: NixBackendType,
161+
#[setting(nested)]
162+
#[serde(skip_serializing_if = "Option::is_none", default)]
163+
pub secretspec: Option<SecretspecConfig>,
164+
}
165+
166+
#[derive(schematic::Config, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
167+
pub struct SecretspecConfig {
168+
#[serde(skip_serializing_if = "is_false", default = "false_default")]
169+
#[setting(default = false)]
170+
pub enable: bool,
171+
#[serde(skip_serializing_if = "Option::is_none", default)]
172+
pub profile: Option<String>,
173+
#[serde(skip_serializing_if = "Option::is_none", default)]
174+
pub provider: Option<String>,
161175
}
162176

163177
// TODO: https://github.com/moonrepo/schematic/issues/105

devenv/src/devenv.rs

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use cli_table::{print_stderr, WithTitle};
77
use include_dir::{include_dir, Dir};
88
use miette::{bail, miette, Context, IntoDiagnostic, Result};
99
use once_cell::sync::Lazy;
10+
use secretspec;
1011
use serde::Deserialize;
1112
use serde_json;
1213
use sha2::Digest;
@@ -90,6 +91,9 @@ pub struct Devenv {
9091

9192
has_processes: Arc<OnceCell<bool>>,
9293

94+
// Secretspec resolved data to pass to Nix
95+
secretspec_resolved: Arc<OnceCell<secretspec::Resolved<HashMap<String, String>>>>,
96+
9397
// TODO: make private.
9498
// Pass as an arg or have a setter.
9599
pub container_name: Option<String>,
@@ -145,11 +149,19 @@ impl Devenv {
145149
cachix_trusted_keys,
146150
};
147151

152+
// Create shared secretspec_resolved Arc to share between Devenv and Nix
153+
let secretspec_resolved = Arc::new(OnceCell::new());
154+
148155
let nix: Box<dyn nix_backend::NixBackend> = match backend_type {
149156
config::NixBackendType::Nix => Box::new(
150-
crate::nix::Nix::new(options.config.clone(), global_options.clone(), paths)
151-
.await
152-
.expect("Failed to initialize Nix backend"),
157+
crate::nix::Nix::new(
158+
options.config.clone(),
159+
global_options.clone(),
160+
paths,
161+
secretspec_resolved.clone(),
162+
)
163+
.await
164+
.expect("Failed to initialize Nix backend"),
153165
),
154166
#[cfg(feature = "snix")]
155167
config::NixBackendType::Snix => Box::new(
@@ -176,6 +188,7 @@ impl Devenv {
176188
assembled: Arc::new(AtomicBool::new(false)),
177189
assemble_lock: Arc::new(Semaphore::new(1)),
178190
has_processes: Arc::new(OnceCell::new()),
191+
secretspec_resolved,
179192
container_name: None,
180193
}
181194
}
@@ -1201,6 +1214,79 @@ impl Devenv {
12011214
miette::miette!("Failed to create {}: {}", self.devenv_runtime.display(), e)
12021215
})?;
12031216

1217+
// Check for secretspec.toml and load secrets
1218+
let secretspec_path = self.devenv_root.join("secretspec.toml");
1219+
let secretspec_config_exists = config.secretspec.is_some();
1220+
let secretspec_enabled = config
1221+
.secretspec
1222+
.as_ref()
1223+
.map(|c| c.enable)
1224+
.unwrap_or(false); // Default to false if secretspec config is not present
1225+
1226+
if secretspec_path.exists() {
1227+
// Log warning when secretspec.toml exists but is not configured
1228+
if !secretspec_enabled && !secretspec_config_exists {
1229+
info!(
1230+
"{}",
1231+
indoc::formatdoc! {"
1232+
Found secretspec.toml but secretspec integration is not enabled.
1233+
1234+
To enable, add to devenv.yaml:
1235+
secretspec:
1236+
enable: true
1237+
1238+
To disable this message:
1239+
secretspec:
1240+
enable: false
1241+
1242+
Learn more: https://devenv.sh/integrations/secretspec/
1243+
"}
1244+
);
1245+
}
1246+
1247+
if secretspec_enabled {
1248+
// Get profile and provider from devenv.yaml config
1249+
let (profile, provider) = if let Some(ref secretspec_config) = config.secretspec {
1250+
(
1251+
secretspec_config.profile.clone(),
1252+
secretspec_config.provider.clone(),
1253+
)
1254+
} else {
1255+
(None, None)
1256+
};
1257+
1258+
// Load and validate secrets using SecretSpec API
1259+
let mut secrets = secretspec::Secrets::load()
1260+
.map_err(|e| miette!("Failed to load secretspec configuration: {}", e))?;
1261+
1262+
// Configure provider and profile if specified
1263+
if let Some(ref provider_str) = provider {
1264+
secrets.set_provider(provider_str);
1265+
}
1266+
if let Some(ref profile_str) = profile {
1267+
secrets.set_profile(profile_str);
1268+
}
1269+
1270+
// Validate secrets
1271+
match secrets.validate()? {
1272+
Ok(validated_secrets) => {
1273+
// Store resolved secrets in OnceCell for Nix to use
1274+
self.secretspec_resolved
1275+
.set(validated_secrets.resolved)
1276+
.map_err(|_| miette!("Secretspec resolved already set"))?;
1277+
}
1278+
Err(validation_errors) => {
1279+
bail!(
1280+
"Required secrets are missing: {} (provider: {}, profile: {})",
1281+
validation_errors.missing_required.join(", "),
1282+
validation_errors.provider,
1283+
validation_errors.profile
1284+
);
1285+
}
1286+
}
1287+
}
1288+
}
1289+
12041290
// Create cli-options.nix if there are CLI options
12051291
if !self.global_options.option.is_empty() {
12061292
let mut cli_options = String::from("{ pkgs, lib, config, ... }: {\n");

devenv/src/nix.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ use async_trait::async_trait;
44
use futures::future;
55
use miette::{bail, IntoDiagnostic, Result, WrapErr};
66
use nix_conf_parser::NixConf;
7+
use secretspec;
78
use serde::Deserialize;
9+
use serde_json;
810
use sqlx::SqlitePool;
9-
use std::collections::BTreeMap;
11+
use std::collections::{BTreeMap, HashMap};
1012
use std::env;
1113
use std::os::unix::fs::symlink;
1214
use std::os::unix::process::CommandExt;
@@ -27,13 +29,15 @@ pub struct Nix {
2729
global_options: cli::GlobalOptions,
2830
cachix_caches: Arc<OnceCell<CachixCaches>>,
2931
paths: nix_backend::DevenvPaths,
32+
secretspec_resolved: Arc<OnceCell<secretspec::Resolved<HashMap<String, String>>>>,
3033
}
3134

3235
impl Nix {
3336
pub async fn new(
3437
config: config::Config,
3538
global_options: cli::GlobalOptions,
3639
paths: nix_backend::DevenvPaths,
40+
secretspec_resolved: Arc<OnceCell<secretspec::Resolved<HashMap<String, String>>>>,
3741
) -> Result<Self> {
3842
let cachix_caches = Arc::new(OnceCell::new());
3943
let options = nix_backend::Options::default();
@@ -51,6 +55,7 @@ impl Nix {
5155
global_options,
5256
cachix_caches,
5357
paths,
58+
secretspec_resolved,
5459
})
5560
}
5661

@@ -609,6 +614,19 @@ impl Nix {
609614
// set a dummy value to overcome https://github.com/NixOS/nix/issues/10247
610615
cmd.env("NIX_PATH", ":");
611616
}
617+
618+
// Pass secretspec data to Nix if available
619+
if let Some(resolved) = self.secretspec_resolved.get() {
620+
let secrets_data = serde_json::json!({
621+
"secrets": resolved.secrets,
622+
"profile": resolved.profile,
623+
"provider": resolved.provider
624+
});
625+
if let Ok(secrets_json) = serde_json::to_string(&secrets_data) {
626+
cmd.env("SECRETSPEC_SECRETS", secrets_json);
627+
}
628+
}
629+
612630
cmd.args(flags);
613631
cmd.current_dir(&self.paths.root);
614632

docs/devenv.schema.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@
5252
"items": {
5353
"type": "string"
5454
}
55+
},
56+
"secretspec": {
57+
"anyOf": [
58+
{
59+
"$ref": "#/definitions/SecretspecConfig"
60+
},
61+
{
62+
"type": "null"
63+
}
64+
]
5565
}
5666
},
5767
"definitions": {
@@ -168,6 +178,26 @@
168178
}
169179
}
170180
}
181+
},
182+
"SecretspecConfig": {
183+
"type": "object",
184+
"properties": {
185+
"enable": {
186+
"type": "boolean"
187+
},
188+
"profile": {
189+
"type": [
190+
"string",
191+
"null"
192+
]
193+
},
194+
"provider": {
195+
"type": [
196+
"string",
197+
"null"
198+
]
199+
}
200+
}
171201
}
172202
}
173203
}

docs/integrations/.nav.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
nav:
22
- .env: dotenv.md
3+
- secretspec: secretspec.md
34
- Android: android.md
45
- Wordpress: wordpress.md
56
- GitHub Actions: github-actions.md

docs/integrations/secretspec.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# SecretSpec
2+
3+
[SecretSpec](https://secretspec.dev) separates secret declaration from secret provisioning. You define what secrets your application needs in a `secretspec.toml` file, and each developer, CI system, and production environment can provide those secrets from their preferred secure provider.
4+
5+
## Quick Start
6+
7+
Follow [SecretSpec Quick Start](https://secretspec.dev/quick-start/).
8+
9+
## Best Practice: Runtime Loading
10+
11+
While you can enable SecretSpec in devenv to load secrets into `secretspec.secrets` option, we recommend:
12+
13+
a) [Use Rust SDK](https://secretspec.dev/sdk/rust/)
14+
15+
b) Your application load secrets at runtime instead:
16+
17+
```bash
18+
$ devenv shell
19+
$ secretspec run -- npm start
20+
```
21+
22+
This approach:
23+
- Keeps secrets out of your shell environment
24+
- Reduces exposure of sensitive data
25+
- Makes secret rotation easier
26+
- Follows the principle of least privilege
27+
28+
## Configuration (Optional)
29+
30+
If you do need secrets in your devenv environment:
31+
32+
```yaml title="devenv.yaml"
33+
secretspec:
34+
enable: true
35+
# these are optional global overrides
36+
provider: keyring # keyring, dotenv, env, 1password, lastpass
37+
profile: default # profile from secretspec.toml
38+
```
39+
40+
Then access in `devenv.nix`:
41+
42+
```nix title="devenv.nix"
43+
{ config, ... }:
44+
45+
{
46+
env.DATABASE_URL = config.secretspec.secrets.DATABASE_URL or "";
47+
}
48+
```
49+
50+
## Learn More
51+
52+
- [secretspec.dev](https://secretspec.dev)
53+
- [Providers](https://secretspec.dev/docs/providers) - Keyring, 1Password, dotenv, and more
54+
- [Profiles](https://secretspec.dev/docs/profiles) - Environment-specific configurations
55+
- [Rust SDK](https://secretspec.dev/docs/rust-sdk) - Type-safe secret access

0 commit comments

Comments
 (0)