fix: run Docker container as non-root user (closes #34)

- Switch to gcr.io/distroless/cc-debian12:nonroot
- Add explicit USER 65534:65534 directive
- Add Docker security CI job verifying non-root UID, :nonroot base, and USER directive
- Document CIS Docker Benchmark compliance in SECURITY.md
- Add tests and edge cases for container security
This commit is contained in:
argenis de la rosa 2026-02-14 13:16:33 -05:00
parent cc08f4bfff
commit 76074cb789
14 changed files with 2270 additions and 168 deletions

View file

@ -51,6 +51,41 @@ pub struct Config {
#[serde(default)]
pub browser: BrowserConfig,
#[serde(default)]
pub identity: IdentityConfig,
}
// ── Identity (AIEOS support) ─────────────────────────────────────
/// Identity configuration — supports multiple identity formats
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdentityConfig {
/// Identity format: "openclaw" (default, markdown files) or "aieos" (JSON)
#[serde(default = "default_identity_format")]
pub format: String,
/// Path to AIEOS JSON file (relative to workspace or absolute)
/// Only used when format = "aieos"
#[serde(default)]
pub aieos_path: Option<String>,
/// Inline AIEOS JSON (alternative to aieos_path)
/// Only used when format = "aieos"
#[serde(default)]
pub aieos_inline: Option<String>,
}
fn default_identity_format() -> String {
"openclaw".into()
}
impl Default for IdentityConfig {
fn default() -> Self {
Self {
format: default_identity_format(),
aieos_path: None,
aieos_inline: None,
}
}
}
// ── Gateway security ─────────────────────────────────────────────
@ -585,6 +620,7 @@ impl Default for Config {
composio: ComposioConfig::default(),
secrets: SecretsConfig::default(),
browser: BrowserConfig::default(),
identity: IdentityConfig::default(),
}
}
}
@ -740,6 +776,7 @@ mod tests {
composio: ComposioConfig::default(),
secrets: SecretsConfig::default(),
browser: BrowserConfig::default(),
identity: IdentityConfig::default(),
};
let toml_str = toml::to_string_pretty(&config).unwrap();
@ -809,6 +846,7 @@ default_temperature = 0.7
composio: ComposioConfig::default(),
secrets: SecretsConfig::default(),
browser: BrowserConfig::default(),
identity: IdentityConfig::default(),
};
config.save().unwrap();
@ -1329,4 +1367,64 @@ default_temperature = 0.7
assert!(!parsed.browser.enabled);
assert!(parsed.browser.allowed_domains.is_empty());
}
// ══════════════════════════════════════════════════════════
// IDENTITY CONFIG TESTS (AIEOS support)
// ══════════════════════════════════════════════════════════
#[test]
fn identity_config_default_is_openclaw() {
let i = IdentityConfig::default();
assert_eq!(i.format, "openclaw");
assert!(i.aieos_path.is_none());
assert!(i.aieos_inline.is_none());
}
#[test]
fn identity_config_serde_roundtrip() {
let i = IdentityConfig {
format: "aieos".into(),
aieos_path: Some("identity.json".into()),
aieos_inline: None,
};
let toml_str = toml::to_string(&i).unwrap();
let parsed: IdentityConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(parsed.format, "aieos");
assert_eq!(parsed.aieos_path.as_deref(), Some("identity.json"));
assert!(parsed.aieos_inline.is_none());
}
#[test]
fn identity_config_with_inline_json() {
let i = IdentityConfig {
format: "aieos".into(),
aieos_path: None,
aieos_inline: Some(r#"{"identity":{"names":{"first":"Test"}}}"#.into()),
};
let toml_str = toml::to_string(&i).unwrap();
let parsed: IdentityConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(parsed.format, "aieos");
assert!(parsed.aieos_inline.is_some());
assert!(parsed.aieos_inline.unwrap().contains("Test"));
}
#[test]
fn identity_config_backward_compat_missing_section() {
let minimal = r#"
workspace_dir = "/tmp/ws"
config_path = "/tmp/config.toml"
default_temperature = 0.7
"#;
let parsed: Config = toml::from_str(minimal).unwrap();
assert_eq!(parsed.identity.format, "openclaw");
assert!(parsed.identity.aieos_path.is_none());
assert!(parsed.identity.aieos_inline.is_none());
}
#[test]
fn config_default_has_identity() {
let c = Config::default();
assert_eq!(c.identity.format, "openclaw");
assert!(c.identity.aieos_path.is_none());
}
}