Skip to main content

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