Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c186ab0
refactor(config): centralize runtime bootstrap snapshot
codex Sep 1, 2026
64dc67b
fix(config): make bootstrap tests deterministic and recursive
seonghobae Sep 1, 2026
7a5b410
fix(config): keep credential locator in secret bootstrap boundary
seonghobae Sep 1, 2026
492fba1
merge: synchronize runtime configuration refactor with protected main
seonghobae Sep 1, 2026
43d1b6e
fix(ci): apply rustfmt to runtime configuration
seonghobae Sep 1, 2026
b2c73d9
fix(config): scope Path import to architecture tests
seonghobae Sep 3, 2026
46ede01
fix(config): declare tempfile test dependency
seonghobae Sep 3, 2026
19d82ca
fix(config): keep architecture test dependency-free
seonghobae Sep 3, 2026
9389a2d
fix(config): make nested env-read regression hermetic
seonghobae Sep 3, 2026
c95c301
docs(config): clarify runtime snapshot migration
codex Sep 4, 2026
d9c00aa
docs(config): ground bootstrap authority split
codex Sep 4, 2026
6b0219d
docs(config): tighten bootstrap coverage notes
codex Sep 4, 2026
fd9e86b
docs(runtime): cover bootstrap helpers
codex Sep 4, 2026
45733f0
chore: integrate protected workflow foundation into runtime config
seonghobae Sep 4, 2026
0f22aaf
test(config): reject zero runtime resource bounds
seonghobae Sep 4, 2026
d28a011
fix(config): fail closed on zero runtime bounds
seonghobae Sep 4, 2026
054c11a
docs: raise runtime bootstrap doc coverage
codex Sep 4, 2026
a904558
merge(main): adopt protected anti-bot boundary into runtime configura…
seonghobae Sep 6, 2026
2ae4ee7
test(config): reject aliased runtime env reads
seonghobae Sep 7, 2026
13da592
fix(config): detect imported runtime env aliases
seonghobae Sep 7, 2026
520db29
test(config): expose std alias env-read bypass
seonghobae Sep 8, 2026
3d93314
fix(config): reject std-root runtime env aliases
seonghobae Sep 8, 2026
0f9ad5c
test(config): expose grouped std-root alias bypass
seonghobae Sep 8, 2026
7765da4
fix(config): reject extern-crate std root aliases
seonghobae Sep 8, 2026
e282257
fix(config): reject grouped std-root aliases
seonghobae Sep 8, 2026
93a51f9
style(config): apply rustfmt EOF newline
seonghobae Sep 8, 2026
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
6 changes: 5 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ flowchart LR

## Components

- `src/main.rs`: process startup and operator configuration from `BIND_ADDR`, `ADMIN_TOKEN`, `WAF_IDS_STATE_PATH`, `DNSBL_ORIGIN`, and `EVENT_LIMIT`.
- `src/runtime_config.rs`: runtime-configuration supporting subdomain bootstrap. Reads non-secret process settings from env once, validates them into an immutable `RuntimeConfiguration`, and passes that snapshot inward to `run_from_env`.
- `src/credentials.rs`: secret bootstrap adapter. Reads `ADMIN_TOKEN`, `ADMIN_TOKENS`, and optional `WAF_IDS_CREDENTIALS_PATH` only at the process edge, then exposes a process-local `CredentialRegistry`.

- `src/main.rs`: thin process entrypoint and shutdown-signal installation.
- `src/lib.rs`: Axum app, routing, management APIs, optional JSON persistence, gateway handler, upstream proxying, admin console, support bundle assembly, NDJSON event export, and in-crate HTTP tests.
- `crates/waf-ids-core`: reusable domain models plus validation, upsert, scoring, DNSBL zone export, event retention, threat-feed freshness, KPI snapshot, and commercial readiness logic.
- `/admin`: embedded web console.
Expand Down Expand Up @@ -59,6 +62,7 @@ flowchart LR

- Default bind address is localhost.
- Remote management requires `ADMIN_TOKEN` plus external TLS and identity controls.
- Runtime configuration is loaded once at bootstrap and handed inward as an immutable snapshot; application code does not read operational env vars directly.
- `WAF_IDS_STATE_PATH` enables JSON state persistence for standalone operation. Without it, the service uses seeded in-memory state.
- File-backed writes use temporary sibling files followed by atomic rename. Management API mutations roll back in memory if the state file cannot be replaced.
- Block mode is route-scoped to avoid global accidental enforcement.
Expand Down
18 changes: 17 additions & 1 deletion src/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
//! [`CredentialRegistry::get_credential`].

use serde::{Deserialize, Serialize};
use std::{collections::HashMap, io::ErrorKind, path::Path};
use std::{
collections::HashMap,
io::ErrorKind,
path::{Path, PathBuf},
};

/// Well-known credentials loaded into the registry at bootstrap.
pub const CRED_ADMIN_TOKEN: &str = "admin_token";
Expand Down Expand Up @@ -65,6 +69,18 @@ impl CredentialRegistry {
.is_some_and(|v| !v.trim().is_empty())
}

pub fn bootstrap_from_env() -> Result<(Self, Option<PathBuf>), String> {
let credentials_path = std::env::var("WAF_IDS_CREDENTIALS_PATH")
.ok()
.map(PathBuf::from);
let registry = Self::bootstrap_secrets(
credentials_path.as_deref(),
std::env::var("ADMIN_TOKEN").ok(),
std::env::var("ADMIN_TOKENS").ok(),
)?;
Ok((registry, credentials_path))
}

/// Bootstrap secret-bearing credentials plus the optional KEV fetch override.
///
/// Precedence: JSON credentials file (when present) wins per-key; missing
Expand Down
102 changes: 8 additions & 94 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ mod credentials;
mod kev_import;
mod misp_import;
mod opencti_import;
mod runtime_config;
mod stix_import;
mod suricata_eve;
mod taxii;
pub use credentials::{CRED_ADMIN_TOKEN, CRED_ADMIN_TOKENS, CredentialRegistry, CredentialSource};
pub use runtime_config::{RuntimeConfiguration, parse_event_limit, parse_u32_env, parse_u64_env};

#[derive(Clone)]
pub struct AppState {
Expand Down Expand Up @@ -3208,66 +3210,6 @@ initSocLlm();
</body>
</html>"##;

/// Parse the `EVENT_LIMIT` value (already read from the environment as an
/// optional string). Absent falls back to [`AppConfig::DEFAULT_EVENT_LIMIT`]; a
/// non-integer or zero value is a hard configuration error. Kept in the library
/// (rather than the binary) so it is exercised by unit tests.
pub fn parse_event_limit(raw: Option<&str>) -> Result<usize, Box<dyn std::error::Error>> {
let value = match raw {
Some(raw) => raw.parse::<usize>().map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("EVENT_LIMIT must be a positive integer, got {raw:?}: {error}"),
)
})?,
None => AppConfig::DEFAULT_EVENT_LIMIT,
};
if value == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"EVENT_LIMIT must be greater than 0",
)
.into());
}
Ok(value)
}

/// Parse a `u32` environment value (already read as an optional string),
/// returning `default` when absent and a configuration error when malformed.
pub fn parse_u32_env(
name: &str,
raw: Option<&str>,
default: u32,
) -> Result<u32, Box<dyn std::error::Error>> {
match raw {
Some(raw) => Ok(raw.parse::<u32>().map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("{name} must be a non-negative integer, got {raw:?}: {error}"),
)
})?),
None => Ok(default),
}
}

/// Parse a `u64` environment value (already read as an optional string),
/// returning `default` when absent and a configuration error when malformed.
pub fn parse_u64_env(
name: &str,
raw: Option<&str>,
default: u64,
) -> Result<u64, Box<dyn std::error::Error>> {
match raw {
Some(raw) => Ok(raw.parse::<u64>().map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("{name} must be a positive integer, got {raw:?}: {error}"),
)
})?),
None => Ok(default),
}
}

/// Read gateway configuration from the process environment, bind the listener,
/// and serve until `shutdown` resolves. The binary entrypoint is a thin shim
/// over this function so every branch is reachable from tests (the parse/error
Expand All @@ -3276,43 +3218,15 @@ pub fn parse_u64_env(
pub async fn run_from_env(
shutdown: std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>,
) -> Result<(), Box<dyn std::error::Error>> {
let bind_addr = std::env::var("BIND_ADDR").unwrap_or_else(|_| "127.0.0.1:8080".to_string());
// Secret-bearing values go through the credential registry (env/file are
// bootstrap transports only). Operational config remains env for now.
let credentials_path = std::env::var("WAF_IDS_CREDENTIALS_PATH")
.ok()
.map(PathBuf::from);
let credentials = CredentialRegistry::bootstrap_secrets(
credentials_path.as_deref(),
std::env::var("ADMIN_TOKEN").ok(),
std::env::var("ADMIN_TOKENS").ok(),
)?;
let config = AppConfig {
admin_token: credentials
.get_credential(CRED_ADMIN_TOKEN)
.map(str::to_owned),
state_path: std::env::var("WAF_IDS_STATE_PATH").ok().map(PathBuf::from),
dnsbl_origin: std::env::var("DNSBL_ORIGIN")
.unwrap_or_else(|_| AppConfig::DEFAULT_DNSBL_ORIGIN.to_string()),
event_limit: parse_event_limit(std::env::var("EVENT_LIMIT").ok().as_deref())?,
};
let rate_limit = parse_u32_env("RATE_LIMIT", std::env::var("RATE_LIMIT").ok().as_deref(), 0)?;
let rate_limit_window = parse_u64_env(
"RATE_LIMIT_WINDOW",
std::env::var("RATE_LIMIT_WINDOW").ok().as_deref(),
60,
)?;
let runtime = RuntimeConfiguration::from_env()?;
let (credentials, _) = CredentialRegistry::bootstrap_from_env()?;
Comment thread
seonghobae marked this conversation as resolved.
let config = runtime.app_config(&credentials);
let admin_tokens = parse_admin_tokens(
credentials
.get_credential(CRED_ADMIN_TOKENS)
.unwrap_or_default(),
);
let max_body_bytes = parse_u64_env(
"MAX_BODY_BYTES",
std::env::var("MAX_BODY_BYTES").ok().as_deref(),
1_048_576,
)? as usize;
let listener = tokio::net::TcpListener::bind(&bind_addr).await?;
let listener = tokio::net::TcpListener::bind(&runtime.bind_addr).await?;
let local_addr = listener.local_addr()?;
println!("waf-ids-ai-soc listening on http://{local_addr}");
// Flush so a supervising parent process (the e2e test) sees the readiness
Expand All @@ -3321,10 +3235,10 @@ pub async fn run_from_env(
let state = AppState::load(config)
.await
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidData, message))?
.with_rate_limit(rate_limit, rate_limit_window)
.with_rate_limit(runtime.rate_limit, runtime.rate_limit_window)
.with_admin_tokens(admin_tokens)
.with_credentials_source(credentials.source())
.with_max_body_size(max_body_bytes);
.with_max_body_size(runtime.max_body_bytes);
let served = axum::serve(listener, build_app(state))
.with_graceful_shutdown(shutdown)
.await;
Expand Down
Loading
Loading