Skip to content
Draft
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
1 change: 1 addition & 0 deletions crates/cdk-integration-tests/src/bin/start_fake_mint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ async fn start_fake_mint(
port: 15060,
tls_dir: Some(temp_dir.to_path_buf()),
allow_insecure: false,
keyset_rotation_interval_seconds: None,
})
} else {
None
Expand Down
15 changes: 15 additions & 0 deletions crates/cdk-mintd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,21 @@ cdk-mint-cli rotate-next-keyset --use-keyset-v2 true # Rotate to V2
cdk-mint-cli rotate-next-keyset --use-keyset-v2 false # Rotate to V1
```

**Automatic Rotation:**
An embedded signatory rotates active keysets automatically once they reach a
given age. The replacement keeps the previous amounts, input fee and version.
Meant for long periods (days); the default is 90 days.

- **Default**: active keysets rotate once they are 90 days old (7776000
seconds).
- `[signatory].keyset_rotation_interval_seconds = <seconds>` (or
`CDK_MINTD_SIGNATORY_KEYSET_ROTATION_INTERVAL_SECONDS=<seconds>`): override the
interval.
- Set the value to `0` to disable auto-rotation.

This applies only to an embedded signatory; a remote signatory manages its own
rotation schedule.

## Production Examples

### With LDK Node (Recommended for Testing)
Expand Down
6 changes: 6 additions & 0 deletions crates/cdk-mintd/example.config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ enabled = false
# tls_dir = "/path/to/tls"
# allow_insecure = false

# Automatically rotate active keysets once they reach this age, in seconds.
# Applies to the embedded signatory (enabled = false); a remote signatory
# rotates on its own schedule. Defaults to 7776000 (90 days); set to 0 to
# disable auto-rotation.
# keyset_rotation_interval_seconds = 7776000

# Optional existing management RPC for immediate mint metadata and keyset
# operations. Configuration commands access the database directly.
[mint_management_rpc]
Expand Down
15 changes: 15 additions & 0 deletions crates/cdk-mintd/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ pub struct Signatory {
pub tls_dir: Option<PathBuf>,
#[serde(default)]
pub allow_insecure: bool,
/// Automatically rotate active keysets once they reach this age, in seconds.
///
/// Applies to the embedded signatory the mint runs when `enabled` is false.
/// Defaults to 90 days; set to `0` to disable auto-rotation. A remote
/// signatory (`enabled = true`) manages its own rotation schedule and
/// ignores this value.
#[serde(default = "default_keyset_rotation_interval_seconds")]
pub keyset_rotation_interval_seconds: Option<u64>,
}

impl Default for Signatory {
Expand All @@ -147,6 +155,7 @@ impl Default for Signatory {
port: default_signatory_port(),
tls_dir: None,
allow_insecure: false,
keyset_rotation_interval_seconds: default_keyset_rotation_interval_seconds(),
}
}
}
Expand All @@ -155,6 +164,12 @@ fn default_signatory_address() -> String {
"127.0.0.1".to_string()
}

/// Default keyset auto-rotation interval: 90 days, matching common mint
/// deployments. Set the config value to `0` to disable.
fn default_keyset_rotation_interval_seconds() -> Option<u64> {
Some(90 * 24 * 60 * 60)
}

fn default_signatory_port() -> u16 {
15060
}
Expand Down
1 change: 1 addition & 0 deletions crates/cdk-mintd/src/config_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,7 @@ fn apply_released_v017_signatory(
port,
allow_insecure: tls_dir.is_none(),
tls_dir,
..Default::default()
});
// Released v0.17 selected the remote signatory before either local source.
// Remove ignored local material so the new mutually-exclusive model keeps
Expand Down
1 change: 1 addition & 0 deletions crates/cdk-mintd/src/config_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,7 @@ engine = "sqlite"
port: 15060,
tls_dir: None,
allow_insecure: true,
..Default::default()
}),
..Default::default()
};
Expand Down
2 changes: 2 additions & 0 deletions crates/cdk-mintd/src/env_vars/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub const ENV_SIGNATORY_ADDRESS: &str = "CDK_MINTD_SIGNATORY_ADDRESS";
pub const ENV_SIGNATORY_PORT: &str = "CDK_MINTD_SIGNATORY_PORT";
pub const ENV_SIGNATORY_TLS_DIR: &str = "CDK_MINTD_SIGNATORY_TLS_DIR";
pub const ENV_SIGNATORY_ALLOW_INSECURE: &str = "CDK_MINTD_SIGNATORY_ALLOW_INSECURE";
pub const ENV_SIGNATORY_KEYSET_ROTATION_INTERVAL_SECONDS: &str =
"CDK_MINTD_SIGNATORY_KEYSET_ROTATION_INTERVAL_SECONDS";
pub const ENV_SECONDS_QUOTE_VALID: &str = "CDK_MINTD_SECONDS_QUOTE_VALID";
pub const ENV_CACHE_SECONDS: &str = "CDK_MINTD_CACHE_SECONDS";
pub const ENV_EXTEND_CACHE_SECONDS: &str = "CDK_MINTD_EXTEND_CACHE_SECONDS";
Expand Down
2 changes: 1 addition & 1 deletion crates/cdk-mintd/src/env_vars/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ impl Settings {
});

self.info = self.info.clone().from_env();
self.signatory = Some(self.signatory.clone().unwrap_or_default().from_env());
self.signatory = Some(self.signatory.clone().unwrap_or_default().from_env()?);

self.mint_info = self.mint_info.clone().from_env();
// CDK_MINTD_PAYMENT_BACKEND_* env vars only apply when there is exactly
Expand Down
45 changes: 42 additions & 3 deletions crates/cdk-mintd/src/env_vars/signatory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

use std::env;

use anyhow::{Context, Result};

use super::common::*;
use crate::config::Signatory;

impl Signatory {
pub fn from_env(mut self) -> Self {
pub fn from_env(mut self) -> Result<Self> {
if let Ok(enabled) = env::var(ENV_SIGNATORY_ENABLED) {
if let Ok(enabled) = enabled.parse() {
self.enabled = enabled;
Expand All @@ -33,7 +35,20 @@ impl Signatory {
}
}

self
// Hard failure rather than a silent fallback: an unparsable value here
// would otherwise leave the 90-day default in place, so an operator
// trying to disable auto-rotation would get keys rotating instead.
if let Ok(interval_str) = env::var(ENV_SIGNATORY_KEYSET_ROTATION_INTERVAL_SECONDS) {
let interval = interval_str.parse().with_context(|| {
format!(
"{ENV_SIGNATORY_KEYSET_ROTATION_INTERVAL_SECONDS} must be a whole number of \
seconds; 0 disables keyset auto-rotation"
)
})?;
self.keyset_rotation_interval_seconds = Some(interval);
}

Ok(self)
}
}

Expand All @@ -53,6 +68,7 @@ mod tests {
env::remove_var(ENV_SIGNATORY_PORT);
env::remove_var(ENV_SIGNATORY_TLS_DIR);
env::remove_var(ENV_SIGNATORY_ALLOW_INSECURE);
env::remove_var(ENV_SIGNATORY_KEYSET_ROTATION_INTERVAL_SECONDS);
}

#[test]
Expand All @@ -65,8 +81,9 @@ mod tests {
env::set_var(ENV_SIGNATORY_PORT, "15061");
env::set_var(ENV_SIGNATORY_TLS_DIR, "/var/lib/cdk/signatory-tls");
env::set_var(ENV_SIGNATORY_ALLOW_INSECURE, "true");
env::set_var(ENV_SIGNATORY_KEYSET_ROTATION_INTERVAL_SECONDS, "7776000");

let signatory = Signatory::default().from_env();
let signatory = Signatory::default().from_env().expect("valid env");

assert!(signatory.enabled);
assert_eq!(signatory.address, "0.0.0.0");
Expand All @@ -76,6 +93,28 @@ mod tests {
Some(PathBuf::from("/var/lib/cdk/signatory-tls"))
);
assert!(signatory.allow_insecure);
assert_eq!(signatory.keyset_rotation_interval_seconds, Some(7776000));

clear_env_vars();
}

/// An operator writing `off` means "disable rotation". Falling back to the
/// 90-day default would silently rotate keys instead, so the parse fails.
#[test]
fn signatory_from_env_rejects_unparsable_rotation_interval() {
let _guard = env_lock();
clear_env_vars();

env::set_var(ENV_SIGNATORY_KEYSET_ROTATION_INTERVAL_SECONDS, "off");

let err = Signatory::default()
.from_env()
.expect_err("an unparsable rotation interval must fail configuration");
assert!(
err.to_string()
.contains(ENV_SIGNATORY_KEYSET_ROTATION_INTERVAL_SECONDS),
"the error must name the offending variable, got: {err}"
);

clear_env_vars();
}
Expand Down
14 changes: 14 additions & 0 deletions crates/cdk-mintd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,19 @@ fn configure_basic_info(settings: &config::Settings, mint_builder: MintBuilder)

builder = builder.with_keyset_v2(settings.info.use_keyset_v2);

// Fall back to the default interval when no `[signatory]` section is
// present, so an embedded mint auto-rotates without explicit config.
builder = builder.with_keyset_rotation_interval(
settings
.signatory
.as_ref()
.map_or_else(
|| crate::config::Signatory::default().keyset_rotation_interval_seconds,
|signatory| signatory.keyset_rotation_interval_seconds,
)
.map(std::time::Duration::from_secs),
);

builder
}
/// Configures payment backends based on the specified backend types
Expand Down Expand Up @@ -3138,6 +3151,7 @@ engine = "sqlite"
port: 15060,
tls_dir: Some("/tmp/certs".into()),
allow_insecure: false,
keyset_rotation_interval_seconds: None,
}),
..Default::default()
};
Expand Down
21 changes: 21 additions & 0 deletions crates/cdk-signatory/src/bin/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use {
std::sync::Arc,
std::time::Duration,
std::{env, fs},
tokio::sync::watch,
tracing_subscriber::EnvFilter,
};

Expand Down Expand Up @@ -100,6 +101,11 @@ struct Cli {
/// another's rotations without a restart.
#[arg(long, default_value = "0")]
keyset_refresh_interval_ms: u64,
/// Automatically rotate active keysets once they reach this age, in seconds.
/// Defaults to 7776000 (90 days), matching the embedded mint. A value of 0
/// disables auto-rotation.
#[arg(long, default_value = "7776000")]
rotation_interval_secs: u64,
}

/// Main function for the signatory standalone binary
Expand Down Expand Up @@ -207,6 +213,21 @@ pub async fn cli_main() -> Result<()> {
.then(|| Duration::from_millis(args.keyset_refresh_interval_ms));
signatory.spawn_keyset_refresh(refresh_interval);

// Hold the shutdown sender for the process lifetime so the rotation loop
// keeps running until the server exits; dropping it would stop rotation.
let _rotation_shutdown = if args.rotation_interval_secs > 0 {
let interval = Duration::from_secs(args.rotation_interval_secs);
tracing::info!(
"Enabling keyset auto-rotation every {}s",
args.rotation_interval_secs
);
let (shutdown_tx, shutdown_rx) = watch::channel(false);
signatory.spawn_auto_rotation(interval, shutdown_rx);
Some(shutdown_tx)
} else {
None
};

let socket_addr = SocketAddr::from_str(&format!("{}:{}", args.listen_addr, args.listen_port))?;

start_grpc_server(signatory, socket_addr, certs).await?;
Expand Down
Loading
Loading