Skip to main content

Security Guidelines

Version: 1.0 Status: Canonical Development Guidelines Last Updated: 2025-11-17

This document defines security guidelines for developing AGEniX components.


Table of Contents

  1. Security-First Mindset
  2. OWASP Top 10 Considerations
  3. Rust-Specific Security
  4. Cryptography
  5. Input Validation
  6. Authentication & Authorization
  7. Dependency Security
  8. Security Checklist

1. Security-First Mindset

Core Principle

Every line of code in AGEniX is security-critical.

AGQ handles authentication, job execution, and worker coordination. AGW executes user-provided plans. AGX processes potentially malicious user input. There is no "non-security-critical" code path.

Threat Model

Assume:

  • Users provide malicious input
  • Network is hostile (even localhost can be compromised)
  • Workers may be compromised
  • Plans may contain injection attempts
  • Time is adversarial (timing attacks)

Defense in Depth

Layer multiple security controls:

  1. Input validation - Reject bad data early
  2. Sandboxing - Isolate execution
  3. Least privilege - Minimum necessary permissions
  4. Fail secure - Default to deny, not allow
  5. Audit logging - Detect and investigate incidents

2. OWASP Top 10 Considerations

2.1 Injection Attacks

Risk: Command injection, SQL/NoSQL injection, RESP protocol injection

Command Injection

❌ NEVER DO THIS:

// DANGEROUS: Direct shell execution with user input
let cmd = format!("ls {}", user_input);
std::process::Command::new("sh").arg("-c").arg(&cmd).spawn();

✅ DO THIS:

// SAFE: Direct process spawn, no shell
use std::process::Command;

fn validate_path(path: &str) -> Result<PathBuf, Error> {
// Validate path doesn't contain shell metacharacters
if path.contains(&['|', ';', '&', '$', '`', '\n'][..]) {
bail!("Invalid path: contains shell metacharacters");
}

// Canonicalize to prevent path traversal
let canonical = PathBuf::from(path).canonicalize()?;

// Ensure it's within allowed directory
if !canonical.starts_with("/allowed/dir") {
bail!("Path outside allowed directory");
}

Ok(canonical)
}

// Execute without shell
let validated_path = validate_path(user_input)?;
Command::new("ls").arg(validated_path).spawn()?;

RESP Protocol Injection

❌ NEVER DO THIS:

// DANGEROUS: Injecting user input into RESP message
let msg = format!("SET {} {}\r\n", user_key, user_value);

✅ DO THIS:

// SAFE: Use proper RESP serialization
use crate::protocol::RespValue;

let command = RespValue::Array(vec![
RespValue::BulkString(b"SET".to_vec()),
RespValue::BulkString(user_key.as_bytes().to_vec()),
RespValue::BulkString(user_value.as_bytes().to_vec()),
]);
let serialized = command.serialize();

2.2 Authentication & Session Management

Requirements:

  • Session keys must be cryptographically random (32+ bytes)
  • Constant-time comparison to prevent timing attacks
  • No session keys in logs
  • Rate limiting on authentication attempts
  • Automatic session expiry (via TTL)

✅ CORRECT:

use ring::rand::{SecureRandom, SystemRandom};
use subtle::ConstantTimeEq;

// Generate session key
fn generate_session_key() -> Result<String, Error> {
let rng = SystemRandom::new();
let mut key = [0u8; 32];
rng.fill(&mut key)?;
Ok(hex::encode(key))
}

// Constant-time comparison
fn validate_session_key(provided: &[u8], expected: &[u8]) -> Result<(), Error> {
if provided.len() != expected.len() {
// Return error without revealing which check failed
bail!("Invalid session key");
}

if provided.ct_eq(expected).into() {
Ok(())
} else {
bail!("Invalid session key");
}
}

2.3 Sensitive Data Exposure

Never log:

  • Session keys
  • Plan contents (may contain credentials)
  • Worker registration tokens
  • Job payloads
  • Error details with sensitive data

✅ CORRECT:

use tracing::{info, error, instrument};

#[instrument(skip(session_key))] // Don't log session_key
async fn authenticate(worker_id: &str, session_key: &[u8]) -> Result<(), Error> {
info!(worker_id, "Authenticating worker");

match validate_session_key(session_key) {
Ok(_) => {
info!(worker_id, "Authentication successful");
Ok(())
}
Err(e) => {
// Log error without exposing key
error!(worker_id, "Authentication failed");
Err(e)
}
}
}

2.4 Resource Exhaustion (DoS)

Implement limits:

  • Connection limits per IP
  • Maximum message size
  • Job queue depth limits
  • Worker registration rate limits
  • Timeout all blocking operations

✅ CORRECT:

use tokio::time::timeout;
use std::time::Duration;

// Always set timeouts
async fn execute_with_timeout<F, T>(future: F) -> Result<T, Error>
where
F: Future<Output = Result<T, Error>>,
{
timeout(Duration::from_secs(30), future)
.await
.map_err(|_| Error::Timeout)?
}

// Rate limiting
use governor::{Quota, RateLimiter};

struct AuthRateLimiter {
limiter: RateLimiter<String, DefaultKeyedStateStore<String>, DefaultClock>,
}

impl AuthRateLimiter {
fn new() -> Self {
let quota = Quota::per_minute(nonzero!(5u32));
Self {
limiter: RateLimiter::keyed(quota),
}
}

fn check(&self, ip: &str) -> Result<(), Error> {
self.limiter
.check_key(&ip.to_string())
.map_err(|_| Error::RateLimitExceeded)?;
Ok(())
}
}

2.5 Security Misconfiguration

Secure defaults:

  • Deny by default
  • No debug endpoints in production
  • Validate all configuration at startup
  • Fail closed, not open

✅ CORRECT:

#[derive(Debug)]
pub struct Config {
/// Listen address (default: localhost only)
pub listen_addr: String,

/// Enable debug endpoints (default: false)
#[cfg(debug_assertions)]
pub enable_debug: bool,

/// Maximum connections (default: 100)
pub max_connections: usize,
}

impl Default for Config {
fn default() -> Self {
Self {
listen_addr: "127.0.0.1:6380".into(), // Localhost only
#[cfg(debug_assertions)]
enable_debug: false, // Disabled by default
max_connections: 100,
}
}
}

impl Config {
pub fn validate(&self) -> Result<(), Error> {
// Ensure not listening on 0.0.0.0 in production
if !cfg!(debug_assertions) && self.listen_addr.starts_with("0.0.0.0") {
bail!("Cannot bind to 0.0.0.0 in production");
}

// Validate connection limit is reasonable
if self.max_connections > 10000 {
bail!("max_connections too high (>10000)");
}

Ok(())
}
}

2.6 Deserialization Vulnerabilities

Validate JSON strictly:

  • Set maximum nesting depth
  • Limit array/object sizes
  • Never use unsafe for deserialization
  • Deny unknown fields

✅ CORRECT:

use serde::{Deserialize, Deserializer};

#[derive(Deserialize)]
#[serde(deny_unknown_fields)] // Reject unexpected fields
struct Plan {
#[serde(deserialize_with = "validate_plan_id")]
id: String,

#[serde(deserialize_with = "validate_tasks")]
tasks: Vec<Task>,
}

fn validate_plan_id<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
let id = String::deserialize(deserializer)?;

// Validate format
if !id.chars().all(|c| c.is_alphanumeric() || c == '-') {
return Err(serde::de::Error::custom("Invalid plan ID format"));
}

// Validate length
if id.len() > 64 {
return Err(serde::de::Error::custom("Plan ID too long"));
}

Ok(id)
}

fn validate_tasks<'de, D>(deserializer: D) -> Result<Vec<Task>, D::Error>
where
D: Deserializer<'de>,
{
let tasks = Vec::<Task>::deserialize(deserializer)?;

// Limit task count
if tasks.len() > 100 {
return Err(serde::de::Error::custom("Too many tasks (max 100)"));
}

Ok(tasks)
}

3. Rust-Specific Security

3.1 Memory Safety

Minimize unsafe blocks:

// ✅ SAFE: Use safe abstractions
fn process_data(data: &[u8]) -> Vec<u8> {
data.iter().map(|&b| b ^ 0xFF).collect()
}

// ❌ UNSAFE: Only use when absolutely necessary
unsafe fn process_data_unsafe(data: *const u8, len: usize) -> Vec<u8> {
// SAFETY: Caller must ensure data is valid for len bytes
std::slice::from_raw_parts(data, len)
.iter()
.map(|&b| b ^ 0xFF)
.collect()
}

If unsafe is required:

  • Document safety invariants
  • Add SAFETY comment explaining why it's safe
  • Minimize unsafe scope
  • Add tests specifically for unsafe code

3.2 Integer Overflow

Use checked arithmetic for security-critical calculations:

✅ CORRECT:

fn calculate_timeout(base: u64, multiplier: u32) -> Result<u64, Error> {
let multiplier_u64 = u64::from(multiplier);
base.checked_mul(multiplier_u64)
.ok_or_else(|| Error::IntegerOverflow)
}

Enable overflow checks in release:

[profile.release]
overflow-checks = true

3.3 Panic Safety

Never panic in production code paths:

❌ BAD:

fn process_job(job_id: &str) -> JobStatus {
let job = jobs.get(job_id).unwrap(); // PANIC if not found
job.status
}

✅ GOOD:

fn process_job(job_id: &str) -> Result<JobStatus, Error> {
let job = jobs.get(job_id)
.ok_or_else(|| Error::JobNotFound(job_id.to_string()))?;
Ok(job.status)
}

4. Cryptography

4.1 Use Established Libraries

✅ USE:

  • ring - Cryptographic primitives
  • rustls - TLS implementation
  • subtle - Constant-time operations

❌ NEVER:

  • Roll your own crypto
  • Use deprecated algorithms (MD5, SHA-1, DES)
  • Implement timing-sensitive code without constant-time guarantees

4.2 Random Number Generation

✅ CORRECT:

use ring::rand::{SecureRandom, SystemRandom};

fn generate_nonce() -> Result<Vec<u8>, Error> {
let rng = SystemRandom::new();
let mut nonce = vec![0u8; 32];
rng.fill(&mut nonce)?;
Ok(nonce)
}

❌ WRONG:

use rand::Rng;

fn generate_nonce() -> Vec<u8> {
let mut rng = rand::thread_rng(); // NOT cryptographically secure
(0..32).map(|_| rng.gen()).collect()
}

4.3 Constant-Time Operations

Use for secret comparison:

use subtle::ConstantTimeEq;

fn compare_secrets(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
a.ct_eq(b).into()
}

5. Input Validation

5.1 Validation Pattern

Always validate at boundaries:

use validator::Validate;

#[derive(Deserialize, Validate)]
struct JobRequest {
#[validate(length(min = 1, max = 64))]
#[validate(regex = "^[a-zA-Z0-9_-]+$")]
job_id: String,

#[validate(length(max = 1048576))] // 1MB max
payload: String,

#[validate(range(min = 1, max = 100))]
task_count: usize,
}

fn handle_job_request(req: JobRequest) -> Result<(), Error> {
// Validate before processing
req.validate()?;

// Now safe to process
process_job(&req)
}

5.2 Common Validation Functions

/// Validate worker ID (alphanumeric + hyphens/underscores only)
pub fn validate_worker_id(id: &str) -> Result<(), Error> {
if id.is_empty() {
bail!("Worker ID cannot be empty");
}

if id.len() > 64 {
bail!("Worker ID too long (max 64 chars)");
}

if !id.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
bail!("Worker ID contains invalid characters");
}

Ok(())
}

/// Validate file path (prevent traversal)
pub fn validate_file_path(path: &str) -> Result<PathBuf, Error> {
let path = PathBuf::from(path);

// Reject absolute paths
if path.is_absolute() {
bail!("Absolute paths not allowed");
}

// Reject parent directory references
for component in path.components() {
if matches!(component, std::path::Component::ParentDir) {
bail!("Path traversal not allowed");
}
}

// Canonicalize and ensure within allowed base
let canonical = path.canonicalize()?;
let allowed_base = PathBuf::from("/allowed/base").canonicalize()?;

if !canonical.starts_with(&allowed_base) {
bail!("Path outside allowed directory");
}

Ok(canonical)
}

6. Authentication & Authorization

6.1 Session Key Management

Generation:

fn generate_session_key() -> Result<String, Error> {
let rng = SystemRandom::new();
let mut key = [0u8; 32];
rng.fill(&mut key)?;
Ok(hex::encode(key))
}

Storage (AGQ):

  • Store hex-encoded in memory only
  • Never persist to disk
  • Clear on shutdown

Distribution (AGX/AGW):

  • Read from environment variable or secure config file
  • Never hardcode
  • Never commit to git

Comparison:

use subtle::ConstantTimeEq;

fn authenticate(provided_key: &[u8], stored_key: &[u8]) -> Result<(), Error> {
if provided_key.len() != stored_key.len() {
bail!("Invalid session key");
}

if provided_key.ct_eq(stored_key).into() {
Ok(())
} else {
bail!("Invalid session key");
}
}

6.2 Authorization

Job ownership:

fn can_update_job(job_id: &str, worker_id: &str) -> Result<(), Error> {
let claimed_by = db.get(format!("job:{}:worker", job_id))?;

if claimed_by != worker_id {
bail!("Worker {} cannot update job claimed by {}", worker_id, claimed_by);
}

Ok(())
}

7. Dependency Security

7.1 Audit Dependencies

Before every commit:

cargo audit

In CI:

- name: Security audit
run: cargo audit

7.2 Minimize Dependencies

Principles:

  • Only add dependencies when necessary
  • Prefer well-maintained crates
  • Review dependency tree: cargo tree
  • Avoid unmaintained crates
  • Check for security advisories

7.3 Pin Versions

Use Cargo.lock:

  • Commit Cargo.lock to git
  • Ensures reproducible builds
  • Prevents automatic updates to vulnerable versions

8. Security Checklist

Before Every PR

  • No user input flows to system commands without validation
  • All input is validated and sanitized
  • Authentication checks are in place where required
  • No secrets in logs or error messages
  • Timeouts set on all I/O operations
  • Integer overflow cannot occur in critical paths
  • Deserialization is safe and bounded
  • No unsafe code (or justified with SAFETY comments)
  • Cryptographic operations use constant-time comparisons
  • Error messages don't leak sensitive data
  • Resource limits enforced (memory, connections, etc.)
  • All error paths tested
  • Security tests added for new attack surfaces
  • cargo audit passes
  • cargo clippy -- -W clippy::security passes

Security Review Questions

  1. What could go wrong if this input is malicious?
  2. What happens if this operation times out?
  3. Could this leak information to an attacker?
  4. Is this comparison timing-safe?
  5. Could an attacker exhaust resources here?
  6. Is this error message too revealing?
  7. Could this panic in production?
  8. Is this the minimal privilege needed?

Component-Specific Guidelines

AGQ Security

  • 100% test coverage on authentication
  • Fuzz RESP parser
  • Rate limit all commands
  • Validate all RESP inputs
  • Constant-time session key comparison
  • No plan contents in logs

AGW Security

  • Validate all tool names against allowlist
  • Sanitize all arguments before execution
  • Never use shell for command execution
  • Enforce timeouts on all tasks
  • Capture and sanitize stdout/stderr
  • No network access during execution (future: network namespace)

AGX Security

  • Validate all user prompts (length limits)
  • Sanitize plan JSON before submission
  • Never execute plans locally without user confirmation
  • Validate tool registry responses
  • Rate limit LLM API calls

Reporting Security Vulnerabilities

DO NOT open public issues for security vulnerabilities.

Instead:

  1. Email: security@agenix.sh
  2. Or create private security advisory on GitHub
  3. Include:
    • Description of vulnerability
    • Steps to reproduce
    • Potential impact
    • Suggested fix (if any)

Response time: Within 48 hours



Maintained by: AGX Core Team Review cycle: Quarterly or on security incidents Questions? Open issue with security label or email security@agenix.sh