zebrad/config.rs
1//! Zebrad Config
2//!
3//! See instructions in `commands.rs` to specify the path to your
4//! application's configuration file and/or command-line options
5//! for specifying it.
6
7use std::{collections::HashMap, path::PathBuf};
8
9use serde::{Deserialize, Serialize};
10use zebra_rpc::config::mining::{default_miner_address, MinerAddressType};
11
12use crate::components::With;
13
14/// Centralized, case-insensitive suffix-based deny-list to ban setting config fields with
15/// environment variables if those config field names end with any of these suffixes.
16const DENY_CONFIG_KEY_SUFFIX_LIST: [&str; 5] = [
17 "password",
18 "secret",
19 "token",
20 // Block raw cookies only if a field is literally named "cookie".
21 // (Paths like cookie_dir are not affected.)
22 "cookie",
23 // Only raw private keys; paths like *_private_key_path are not affected.
24 "private_key",
25];
26
27/// Returns true if a leaf key name should be considered sensitive and blocked
28/// from environment variable overrides.
29fn is_sensitive_leaf_key(leaf_key: &str) -> bool {
30 let key = leaf_key.to_ascii_lowercase();
31 DENY_CONFIG_KEY_SUFFIX_LIST
32 .iter()
33 .any(|deny_suffix| key.ends_with(deny_suffix))
34}
35
36/// Configuration for `zebrad`.
37///
38/// The `zebrad` config is a TOML-encoded version of this structure. The meaning
39/// of each field is described in the documentation, although it may be necessary
40/// to click through to the sub-structures for each section.
41///
42/// The path to the configuration file can also be specified with the `--config` flag when running Zebra.
43///
44/// The default path to the `zebrad` config is platform dependent, based on
45/// [`dirs::preference_dir`](https://docs.rs/dirs/latest/dirs/fn.preference_dir.html):
46///
47/// | Platform | Value | Example |
48/// | -------- | ------------------------------------- | ---------------------------------------------- |
49/// | Linux | `$XDG_CONFIG_HOME` or `$HOME/.config` | `/home/alice/.config/zebrad.toml` |
50/// | macOS | `$HOME/Library/Preferences` | `/Users/Alice/Library/Preferences/zebrad.toml` |
51/// | Windows | `{FOLDERID_RoamingAppData}` | `C:\Users\Alice\AppData\Local\zebrad.toml` |
52#[derive(Clone, Default, Debug, Eq, PartialEq, Deserialize, Serialize)]
53#[serde(deny_unknown_fields, default)]
54pub struct ZebradConfig {
55 /// Consensus configuration
56 //
57 // These configs use full paths to avoid a rustdoc link bug (#7048).
58 pub consensus: zebra_consensus::config::Config,
59
60 /// Metrics configuration
61 pub metrics: crate::components::metrics::Config,
62
63 /// Networking configuration
64 pub network: zebra_network::config::Config,
65
66 /// State configuration
67 pub state: zebra_state::config::Config,
68
69 /// Tracing configuration
70 pub tracing: crate::components::tracing::Config,
71
72 /// Sync configuration
73 pub sync: crate::components::sync::Config,
74
75 /// Mempool configuration
76 pub mempool: crate::components::mempool::Config,
77
78 /// Block notify configuration
79 pub notify: crate::components::notify::Config,
80
81 /// RPC configuration
82 pub rpc: zebra_rpc::config::rpc::Config,
83
84 /// Mining configuration
85 pub mining: zebra_rpc::config::mining::Config,
86
87 /// Health check HTTP server configuration.
88 ///
89 /// See the Zebra Book for details and examples:
90 /// <https://zebra.zfnd.org/user/health.html>
91 pub health: crate::components::health::Config,
92
93 /// zcashd-compat mode configuration.
94 pub zcashd_compat: crate::components::zcashd_compat::Config,
95}
96
97impl ZebradConfig {
98 /// Loads the configuration from the conventional sources.
99 ///
100 /// Configuration is loaded from three sources, in order of precedence:
101 /// 1. Environment variables with `ZEBRA_` prefix (highest precedence)
102 /// 2. TOML configuration file (if provided)
103 /// 3. Hard-coded defaults (lowest precedence)
104 ///
105 /// Environment variables use the format `ZEBRA_SECTION__KEY` where:
106 /// - `SECTION` is the configuration section (e.g., `network`, `rpc`)
107 /// - `KEY` is the configuration key within that section
108 /// - Double underscores (`__`) separate nested keys
109 ///
110 /// # Security
111 ///
112 /// Environment variables whose leaf key names end with sensitive suffixes (case-insensitive)
113 /// will cause configuration loading to fail with an error: `password`, `secret`, `token`, `cookie`, `private_key`.
114 /// This prevents both silent misconfigurations and process table exposure of sensitive values.
115 ///
116 /// See [`DENY_CONFIG_KEY_SUFFIX_LIST`] and [`is_sensitive_leaf_key()`] above
117 ///
118 /// # Examples
119 /// - `ZEBRA_NETWORK__NETWORK=Testnet` sets `network.network = "Testnet"`
120 /// - `ZEBRA_RPC__LISTEN_ADDR=127.0.0.1:8232` sets `rpc.listen_addr = "127.0.0.1:8232"`
121 pub fn load(config_path: Option<PathBuf>) -> Result<Self, config::ConfigError> {
122 Self::load_with_env(config_path, "ZEBRA")
123 }
124
125 /// Loads configuration using a caller-provided environment variable prefix.
126 ///
127 /// This allows callers that need multiple configs in the same process (e.g.,
128 /// the `copy-state` command) to keep overrides separate. For example:
129 /// - Source/base config uses `ZEBRA_...` env vars (default prefix)
130 /// - Target config uses `ZEBRA_TARGET_...` env vars
131 ///
132 /// The nested key separator remains `__`, e.g., `ZEBRA_TARGET_STATE__CACHE_DIR`.
133 pub fn load_with_env(
134 config_path: Option<PathBuf>,
135 env_prefix: &str,
136 ) -> Result<Self, config::ConfigError> {
137 // 1. Start with an empty `config::Config` builder (no pre-populated values).
138 // We merge sources, then deserialize into `ZebradConfig`, which uses
139 // `ZebradConfig::default()` wherever keys are missing.
140 let mut builder = config::Config::builder();
141
142 // 2. Add TOML configuration file as a source if provided
143 if let Some(path) = config_path {
144 builder = builder.add_source(
145 config::File::from(path)
146 .format(config::FileFormat::Toml)
147 .required(true),
148 );
149 }
150
151 // 3. Load from environment variables (with a sensitive-leaf deny-list)
152 // Use the provided prefix and `__` as separator for nested keys.
153 // We filter the raw environment first, then let config-rs parse types via try_parsing(true).
154 let mut filtered_env: HashMap<String, String> = HashMap::new();
155 let required_prefix = format!("{}_", env_prefix);
156 for (key, value) in std::env::vars() {
157 if let Some(without_prefix) = key.strip_prefix(&required_prefix) {
158 // Check for sensitive keys on the stripped key.
159 let parts: Vec<&str> = without_prefix.split("__").collect();
160 if let Some(leaf) = parts.last() {
161 if is_sensitive_leaf_key(leaf) {
162 return Err(config::ConfigError::Message(format!(
163 "Environment variable '{}' contains sensitive key '{}' which cannot be overridden via environment variables. \
164 Use the configuration file instead to prevent process table exposure.",
165 key, leaf
166 )));
167 }
168 }
169
170 // When providing a `source` map, the keys should not have the prefix.
171 filtered_env.insert(without_prefix.to_string(), value);
172 }
173 }
174
175 // When using `source`, we provide a map of already-filtered and processed
176 // keys, so we use a default `Environment` without a prefix.
177 builder = builder.add_source(
178 config::Environment::default()
179 .separator("__")
180 .try_parsing(true)
181 .source(Some(filtered_env)),
182 );
183
184 // Build the configuration
185 let config = builder.build()?;
186 // Deserialize into our struct, which will use defaults for any missing fields
187 config.try_deserialize()
188 }
189}
190
191impl With<MinerAddressType> for ZebradConfig {
192 fn with(mut self, miner_address_type: MinerAddressType) -> Self {
193 self.mining.miner_address = Some(
194 default_miner_address(self.network.network.kind(), &miner_address_type)
195 .parse()
196 .expect("valid hard-coded address"),
197 );
198
199 self
200 }
201}