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
26 changes: 25 additions & 1 deletion docs/environments/secrets/sops.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ sops encrypt -i --age "<public key>" .env.json
```

:::: tip
The `-i` overwrites the file. The encrypted file is safe to commit. Set `SOPS_AGE_KEY_FILE=~/.config/mise/age.txt` to decrypt/edit with sops.
The `-i` overwrites the file. The encrypted file is safe to commit. Set `SOPS_AGE_KEY_FILE=~/.config/mise/age.txt` or `MISE_SOPS_AGE_KEY_FILE=~/.config/mise/age.txt` to decrypt/edit with sops.
::::

4. Reference it in config:
Expand All @@ -55,6 +55,30 @@ _.file = ".env.json"

Now `mise env` exposes the values.

## Environment Variables

mise supports both mise-specific environment variables and standard SOPS ones:

**Mise-specific variables (highest priority):**

- `MISE_SOPS_AGE_KEY` - Age private key content directly
- `MISE_SOPS_AGE_KEY_FILE` - Path to age private key file

**Standard SOPS variables (fallback):**

- `SOPS_AGE_KEY_FILE` - Path to age private key file
- `SOPS_AGE_KEY` - Age private key content directly

**Precedence order:**

1. `MISE_SOPS_AGE_KEY` (mise setting or env var, checked first)
2. `MISE_SOPS_AGE_KEY_FILE` or `sops.age_key_file` (mise setting or env var)
3. `SOPS_AGE_KEY_FILE` (standard)
4. `SOPS_AGE_KEY` (standard, direct key content)
5. Default: `~/.config/mise/age.txt`

This allows you to override SOPS settings specifically for mise while keeping your standard SOPS configuration intact for other tools.

## Redaction

Mark secrets from files as sensitive:
Expand Down
13 changes: 11 additions & 2 deletions e2e/secrets/test_secrets
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ set -x

mise use sops age
age="$(mise x -- age-keygen 2>&1)"
age_pub="$(echo "$age" | grep "# public key:" | awk '{print $4}')"
age_pub="$(echo "$age" | grep -E "(Public key:|# public key:)" | head -1 | awk '{print $(NF)}')"
MISE_SOPS_AGE_KEY="$(echo "$age" | grep "AGE-SECRET-KEY")"
export MISE_SOPS_AGE_KEY

Expand All @@ -14,7 +14,11 @@ assert "mise set _.file=.env.json"
assert_contains "mise env" "export SECRET=mysecret"

# yaml
age_pub="$(mise x -- age-keygen -o ~/age.txt 2>&1 | awk '{print $3}')"
unset MISE_SOPS_AGE_KEY
mise unset _.file
rm .env.json
age_output="$(mise x -- age-keygen -o ~/age.txt 2>&1)"
age_pub="$(echo "$age_output" | grep -E "(Public key:|# public key:)" | head -1 | awk '{print $(NF)}')"
mise settings set sops.age_key_file "~/age.txt"
echo 'SECRET: mysecret' >.env.yaml
mise x -- sops encrypt -i --age "$age_pub" .env.yaml
Expand All @@ -25,3 +29,8 @@ assert_contains "mise env" "export SECRET=mysecret"
# sops
mise settings set sops.rops 0
assert_contains "mise env" "export SECRET=mysecret"

# Clear mise settings for remaining tests
mise settings unset sops.age_key_file
unset MISE_SOPS_AGE_KEY
unset SOPS_AGE_KEY
4 changes: 2 additions & 2 deletions schema/mise.json
Original file line number Diff line number Diff line change
Expand Up @@ -1148,11 +1148,11 @@
"additionalProperties": false,
"properties": {
"age_key": {
"description": "The age private key to use for sops secret decryption.",
"description": "The age private key to use for sops secret decryption. Takes precedence over standard SOPS_AGE_KEY environment variable.",
"type": "string"
},
"age_key_file": {
"description": "Path to the age private key file to use for sops secret decryption.",
"description": "Path to the age private key file for sops secret decryption. Takes precedence over standard SOPS_AGE_KEY_FILE environment variable.",
"type": "string"
},
"age_recipients": {
Expand Down
4 changes: 2 additions & 2 deletions settings.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1364,14 +1364,14 @@ env = "MISE_AGE_STRICT"
type = "Bool"

[sops.age_key]
description = "The age private key to use for sops secret decryption."
description = "The age private key to use for sops secret decryption. Takes precedence over standard SOPS_AGE_KEY environment variable."
env = "MISE_SOPS_AGE_KEY"
optional = true
type = "String"

[sops.age_key_file]
default_docs = "~/.config/mise/age.txt"
description = "Path to the age private key file to use for sops secret decryption."
description = "Path to the age private key file for sops secret decryption. Takes precedence over standard SOPS_AGE_KEY_FILE environment variable."
env = "MISE_SOPS_AGE_KEY_FILE"
optional = true
type = "Path"
Expand Down
116 changes: 104 additions & 12 deletions src/sops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,35 +22,97 @@ where
F: rops::file::format::FileFormat,
{
static AGE_KEY: OnceCell<Option<String>> = OnceCell::const_new();
static AGE_KEY_FILE: OnceCell<Option<std::path::PathBuf>> = OnceCell::const_new();
static MUTEX: Mutex<()> = Mutex::const_new(());

let age = AGE_KEY
.get_or_init(async || {
let p = Settings::get()
.sops
.age_key_file
.clone()
.unwrap_or(dirs::CONFIG.join("age.txt"));
// 1. Check mise-specific MISE_SOPS_AGE_KEY setting first (highest priority)
if let Some(age_key) = &Settings::get().sops.age_key
&& !age_key.is_empty()
{
return Some(age_key.clone());
}

// 2. Check mise-specific MISE_SOPS_AGE_KEY_FILE setting
if let Some(key_file) = &Settings::get().sops.age_key_file {
let p = replace_path(
match parse_template(key_file.to_string_lossy().to_string()) {
Ok(p) => p,
Err(e) => {
warn!("failed to parse MISE_SOPS_AGE_KEY_FILE: {}", e);
return None;
}
},
);
if p.exists()
&& let Ok(raw) = file::read_to_string(&p)
{
let key = raw
.trim()
.lines()
.filter(|l| !l.starts_with('#'))
.collect::<String>();
if !key.trim().is_empty() {
// Store the path for later use by sops CLI
let _ = AGE_KEY_FILE.get_or_init(|| async { Some(p.clone()) }).await;
return Some(key);
}
}
}

// 3. Check standard SOPS_AGE_KEY_FILE environment variable
if let Ok(key_file_path) = env::var("SOPS_AGE_KEY_FILE") {
let p = replace_path(match parse_template(key_file_path.clone()) {
Ok(p) => p,
Err(e) => {
warn!("failed to parse SOPS_AGE_KEY_FILE: {}", e);
return None;
}
});
if p.exists()
&& let Ok(raw) = file::read_to_string(&p)
{
let key = raw
.trim()
.lines()
.filter(|l| !l.starts_with('#'))
.collect::<String>();
if !key.trim().is_empty() {
// Store the path for later use by sops CLI
let _ = AGE_KEY_FILE.get_or_init(|| async { Some(p.clone()) }).await;
return Some(key);
}
}
}

// 4. Check standard SOPS_AGE_KEY environment variable (direct key content)
if let Ok(key) = env::var("SOPS_AGE_KEY")
&& !key.trim().is_empty()
{
return Some(key.trim().to_string());
}

// 5. Fall back to default path ~/.config/mise/age.txt
let p = dirs::CONFIG.join("age.txt");
let p = replace_path(match parse_template(p.to_string_lossy().to_string()) {
Ok(p) => p,
Err(e) => {
warn!("failed to parse sops age key file: {}", e);
warn!("failed to parse default sops age key file: {}", e);
return None;
}
});
if let Some(age_key) = &Settings::get().sops.age_key
&& !age_key.is_empty()
{
return Some(age_key.clone());
}
if p.exists()
&& let Ok(raw) = file::read_to_string(p)
&& let Ok(raw) = file::read_to_string(p.clone())
{
let key = raw
.trim()
.lines()
.filter(|l| !l.starts_with('#'))
.collect::<String>();
if !key.trim().is_empty() {
// Store the path for later use by sops CLI
let _ = AGE_KEY_FILE.get_or_init(|| async { Some(p.clone()) }).await;
return Some(key);
}
}
Expand All @@ -70,6 +132,16 @@ where
"SOPS_AGE_KEY"
};
let prev_age = env::var(age_env_key).ok();
let prev_age_key_file = env::var("SOPS_AGE_KEY_FILE").ok();

// Set SOPS_AGE_KEY_FILE with expanded path if we found one, so sops CLI can use it
if let Some(expanded_path) = AGE_KEY_FILE.get().and_then(|f| f.as_ref()) {
env::set_var(
"SOPS_AGE_KEY_FILE",
expanded_path.to_string_lossy().to_string(),
);
}

Comment thread
yordis marked this conversation as resolved.
if let Some(age) = &age {
env::set_var(age_env_key, age.trim());
}
Expand All @@ -87,6 +159,11 @@ where
} else {
env::remove_var(age_env_key);
}
if let Some(age_key_file) = prev_age_key_file {
env::set_var("SOPS_AGE_KEY_FILE", age_key_file);
} else {
env::remove_var("SOPS_AGE_KEY_FILE");
}
return Err(e);
} else {
debug!(
Expand Down Expand Up @@ -116,6 +193,11 @@ where
} else {
env::remove_var(age_env_key);
}
if let Some(age_key_file) = prev_age_key_file {
env::set_var("SOPS_AGE_KEY_FILE", age_key_file);
} else {
env::remove_var("SOPS_AGE_KEY_FILE");
}
return Err(eyre!("sops command not found"));
} else {
debug!("sops command not found, skipping decryption in non-strict mode");
Expand Down Expand Up @@ -145,6 +227,11 @@ where
} else {
env::remove_var(age_env_key);
}
if let Some(age_key_file) = prev_age_key_file {
env::set_var("SOPS_AGE_KEY_FILE", age_key_file);
} else {
env::remove_var("SOPS_AGE_KEY_FILE");
}
return Err(e.into());
} else {
debug!(
Expand All @@ -164,5 +251,10 @@ where
} else {
env::remove_var(age_env_key);
}
if let Some(age_key_file) = prev_age_key_file {
env::set_var("SOPS_AGE_KEY_FILE", age_key_file);
} else {
env::remove_var("SOPS_AGE_KEY_FILE");
}
Ok(output.unwrap_or_default())
}
Loading