DocumentsCipherSwarm
Configuration Initialization Timing
Configuration Initialization Timing
Type
Topic
Status
Published
Created
Feb 27, 2026
Updated
Mar 30, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

Configuration Initialization Timing#

Configuration Initialization Timing is a critical architectural pattern in CipherSwarmAgent that governs how the agent loads, merges, and validates configuration settings from multiple sources during startup. This topic is essential for operators deploying the agent in secure, airgapped environments where configuration must be reliable, predictable, and resilient to filesystem constraints.

CipherSwarmAgent uses Viper, a popular configuration management library, to merge settings from four sources in priority order: command-line flags (highest), environment variables, YAML configuration files, and built-in defaults (lowest). The challenge arises because Viper performs this merging after the application registers its default values but before the agent validates and finalizes its runtime configuration. A common but dangerous mistake is attempting to compute one configuration value from another during the initial default-setting phase—this causes derived values to ignore user-specified overrides, leading to incorrect paths, timeouts, or other critical misconfiguration.

To solve this timing problem, CipherSwarmAgent implements a two-phase initialization architecture. Phase 1 (SetDefaultConfigValues()) registers only simple, literal default values before any configuration files are read. Phase 2 (SetupSharedState()) runs after all configuration sources have been merged, at which point the agent computes derived values (like file paths based on a data directory), validates numeric ranges, and clamps invalid settings to safe defaults with warning messages. This design ensures that operators' configuration choices are always respected while the agent maintains fail-safe operation in restricted environments like read-only filesystems, containerized deployments, or secure lab systems where persistent configuration writes may fail.

Configuration Initialization Sequence#

Complete Bootstrap Flow#

CipherSwarmAgent's configuration initialization follows a precisely ordered sequence:

1. Go runtime executes init() in cmd/root.go
   ├─ Lines 41-159: Define CLI flags and bind to Viper keys
   ├─ Line 37: Register cobra.OnInitialize(config.InitConfig) callback
   └─ Line 161: Call config.SetDefaultConfigValues()
       └─ Register Viper defaults WITHOUT deriving dependent paths

2. main() calls cmd.Execute()
   └─ Cobra framework triggers OnInitialize callback
       └─ config.InitConfig() executes
           ├─ Read config file (if exists)
           ├─ Read environment variables
           └─ Merge with CLI flags and defaults

3. agent.StartAgent() executes
   ├─ Validate required config (api_url, api_token)
   ├─ Line 53: Call config.SetupSharedState()
   │ ├─ Read final merged Viper config
   │ ├─ Derive dependent paths (files_path, zap_path)
   │ ├─ Validate numeric values with clamping
   │ └─ Populate agentstate.State struct
   └─ Call lib.GetAgentConfiguration() to fetch server config
       ├─ Retrieve device lists, update intervals, hashcat settings from API
       └─ Store atomically via lib.SetConfiguration()

4. Runtime goroutines access configuration concurrently
   ├─ Heartbeat goroutine: lib.GetConfiguration() for AgentUpdateInterval
   ├─ Agent-loop goroutine: lib.GetConfiguration() for device lists
   └─ All goroutines: agentstate.State for paths, timeouts, credentials

The critical insight is that SetDefaultConfigValues runs during init() but before the OnInitialize callback that loads config files and environment variables.

Viper Configuration Precedence#

Viper merges configuration from multiple sources with the following precedence (highest to lowest):

  1. Command-line flags (highest priority)
  2. Environment variables
  3. Configuration file (YAML)
  4. Default values (lowest priority)

Each flag is defined using Cobra and bound to a Viper key with the same name. When a key is accessed via viper.Get*(), Viper returns the value from the highest-precedence source that has set it.

SetDefaultConfigValues Function#

The SetDefaultConfigValues function is defined in lib/config/config.go lines 191-217 and registers default values for all configuration keys:

// SetDefaultConfigValues sets default configuration values.
func SetDefaultConfigValues() {
	cwd, err := os.Getwd()
	cobra.CheckErr(err)

	viper.SetDefault("data_path", filepath.Join(cwd, "data"))
	viper.SetDefault("gpu_temp_threshold", DefaultGPUTempThreshold)
	viper.SetDefault("always_use_native_hashcat", false)
	viper.SetDefault("hashcat_path", "")
	viper.SetDefault("sleep_on_failure", DefaultSleepOnFailure)
	viper.SetDefault("always_trust_files", false)
	// files_path and zap_path are derived from data_path in SetupSharedState
	// when not explicitly set (avoids eagerly reading data_path before config is loaded).
	viper.SetDefault("extra_debugging", false)
	viper.SetDefault("status_timer", DefaultStatusTimer)
	viper.SetDefault("heartbeat_interval", DefaultHeartbeatInterval)
	// ... [additional defaults] ...
}

Configuration Default Constants#

All default values are defined as exported constants in lib/config/config.go lines 15-36, serving as the single source of truth:

const (
	// DefaultGPUTempThreshold is the GPU temperature threshold in Celsius.
	DefaultGPUTempThreshold = 80

	// DefaultSleepOnFailure is the sleep duration after task failure.
	DefaultSleepOnFailure = 60 * time.Second

	// DefaultStatusTimer is the status update interval in seconds.
	DefaultStatusTimer = 10

	// DefaultHeartbeatInterval is the heartbeat interval.
	DefaultHeartbeatInterval = 10 * time.Second

	// DefaultTaskTimeout is the task timeout (long-running tasks are expected).
	DefaultTaskTimeout = 24 * time.Hour

	// DefaultDownloadMaxRetries is the max download retry attempts.
	DefaultDownloadMaxRetries = 3

	// DefaultDownloadRetryDelay is the base delay between download retries.
	DefaultDownloadRetryDelay = 2 * time.Second

	// DefaultInsecureDownloads controls TLS certificate verification for downloads.
	DefaultInsecureDownloads = false

	// DefaultMaxHeartbeatBackoff is the max heartbeat backoff multiplier (caps at 64x).
	DefaultMaxHeartbeatBackoff = 6
)

These constants are referenced by both CLI flag defaults and SetDefaultConfigValues, ensuring consistency across the codebase.

The Viper Default Value Computation Pitfall#

GOTCHAS.md explicitly documents the critical timing pitfall:

SetDefaultConfigValues runs before config files/env vars are loaded. Never derive defaults from other viper keys (e.g., viper.GetString("data_path")) — they only return the registered default, not user overrides. Derive in SetupSharedState instead.

Why deriving defaults from other Viper keys fails:

// ❌ WRONG - This will only see the default value of "data_path"
func SetDefaultConfigValues() {
    viper.SetDefault("data_path", "./data")

    // BUG: This reads data_path BEFORE user overrides are loaded
    viper.SetDefault("files_path", filepath.Join(viper.GetString("data_path"), "files"))
}

The problem:

  1. SetDefaultConfigValues executes during init(), before config files/env vars are processed
  2. At this point, viper.GetString("data_path") only returns the just-registered default value "./data"
  3. User overrides to data_path (via CLI flag --data_path=/custom/path, environment variable, or config file) haven't been loaded yet
  4. The derived files_path default is computed from the wrong data_path value
  5. Even if the user sets data_path=/custom/path, files_path would still default to ./data/files instead of /custom/path/files

The solution:

The inline comment at lines 202-203 explains the correct approach:

files_path and zap_path are derived from data_path in SetupSharedState when not explicitly set (avoids eagerly reading data_path before config is loaded).

Notice that files_path and zap_path are not registered as defaults in SetDefaultConfigValues. Instead, they are computed at runtime in SetupSharedState.

SetupSharedState Function#

SetupSharedState is defined in lib/config/config.go lines 79-189 and is called from agent.StartAgent() at line 53, after all configuration sources have been merged.

Derived Path Computation#

SetupSharedState computes derived paths at lines 101-112:

// SetupSharedState configures the shared state from configuration values.
func SetupSharedState() {
	// Set the API URL and token
	agentstate.State.URL = viper.GetString("api_url")
	agentstate.State.APIToken = viper.GetString("api_token")

	dataRoot := viper.GetString("data_path")
	agentstate.State.DataPath = dataRoot
	agentstate.State.PidFile = filepath.Join(dataRoot, "lock.pid")
	agentstate.State.HashcatPidFile = filepath.Join(dataRoot, "hashcat.pid")
	agentstate.State.CrackersPath = filepath.Join(dataRoot, "crackers")

	// ✅ CORRECT - Derived paths computed at runtime
	agentstate.State.FilePath = viper.GetString("files_path")
	if agentstate.State.FilePath == "" {
		agentstate.State.FilePath = filepath.Join(dataRoot, "files")
	}

	agentstate.State.ZapsPath = viper.GetString("zap_path")
	if agentstate.State.ZapsPath == "" {
		agentstate.State.ZapsPath = filepath.Join(dataRoot, "zaps")
	}

	agentstate.State.HashlistPath = filepath.Join(dataRoot, "hashlists")
	agentstate.State.PreprocessorsPath = filepath.Join(dataRoot, "preprocessors")
	agentstate.State.ToolsPath = filepath.Join(dataRoot, "tools")
	agentstate.State.OutPath = filepath.Join(dataRoot, "output")
	agentstate.State.RestoreFilePath = filepath.Join(dataRoot, "restore")
	agentstate.State.BenchmarkCachePath = filepath.Join(dataRoot, "benchmark_cache.json")

	// ... [continue populating state] ...
}

Key behaviors:

  • Check for explicit user values first: viper.GetString("files_path") returns the user's explicit setting (if any)
  • Compute default only if unset: If empty, derive from data_path
  • Respect precedence: User can override data_path OR files_path independently

Test cases in config_test.go lines 233-283 verify this behavior:

  • Default data_path correctly derives files_path and zap_path
  • Custom data_path overrides are honored for derived paths
  • Explicit files_path or zap_path settings override derivation

Configuration Validation#

SetupSharedState validates numeric and duration fields at lines 165-186, clamping invalid values to defaults with warnings:

// Validate numeric/duration config fields — clamp to defaults with a warning.
agentstate.State.DownloadMaxRetries = viper.GetInt("download_max_retries")
if agentstate.State.DownloadMaxRetries < 1 {
	agentstate.Logger.Warn("download_max_retries must be >= 1, using default",
		"configured", agentstate.State.DownloadMaxRetries, "default", DefaultDownloadMaxRetries)
	agentstate.State.DownloadMaxRetries = DefaultDownloadMaxRetries
}

agentstate.State.DownloadRetryDelay = viper.GetDuration("download_retry_delay")

agentstate.State.TaskTimeout = viper.GetDuration("task_timeout")
if agentstate.State.TaskTimeout <= 0 {
	agentstate.Logger.Warn("task_timeout must be > 0, using default",
		"configured", agentstate.State.TaskTimeout, "default", DefaultTaskTimeout)
	agentstate.State.TaskTimeout = DefaultTaskTimeout
}

agentstate.State.MaxHeartbeatBackoff = viper.GetInt("max_heartbeat_backoff")
if agentstate.State.MaxHeartbeatBackoff < 0 {
	agentstate.Logger.Warn("max_heartbeat_backoff must be >= 0, using default",
		"configured", agentstate.State.MaxHeartbeatBackoff, "default", DefaultMaxHeartbeatBackoff)
	agentstate.State.MaxHeartbeatBackoff = DefaultMaxHeartbeatBackoff
}

agentstate.State.SleepOnFailure = viper.GetDuration("sleep_on_failure")

Validation philosophy:

  • Clamp, don't fail: Invalid values are replaced with defaults, not rejected
  • Log warnings: Users are informed of the misconfiguration
  • Continue execution: The agent starts successfully with corrected values
  • Fail-safe defaults: Default values are always valid and safe

This approach is appropriate for an agent designed to run in airgapped environments where interactive configuration debugging may not be feasible.

Configuration Access Patterns#

CipherSwarmAgent supports three patterns for accessing configuration after initialization, each serving different use cases:

Direct Viper Access#

dataPath := viper.GetString("data_path")

Advantages: Simple and direct
Disadvantages:

  • Couples code to Viper library
  • No centralized validation
  • Requires checking validity at every call site

Use case: Reading configuration during initialization in SetupSharedState

AgentState Access#

dataPath := agentstate.State.DataPath

Advantages:

  • Values are pre-validated in SetupSharedState
  • Computed/derived values are available
  • More testable (can mock agentstate.State)
  • Type-safe access (struct fields vs string keys)
  • Decouples application code from Viper

Use case: Accessing agent operational settings (paths, timeouts, thresholds) from any goroutine

Atomic Configuration Access (Required for Server Configuration)#

cfg := lib.GetConfiguration()
backendDevices := cfg.Config.BackendDevices
updateInterval := cfg.Config.AgentUpdateInterval

Advantages:

  • Race-free concurrent access from multiple goroutines
  • Returns immutable snapshot, ensuring consistent reads
  • Atomic updates via lib.SetConfiguration()
  • No locking overhead for readers

Disadvantages:

  • Slightly more verbose than direct global access
  • Returns a copy, not a reference (intentional for safety)

Use case: Accessing server-provided configuration (device lists, update intervals, hashcat settings) from heartbeat and agent-loop goroutines

Implementation: The agent stores server configuration in an atomic.Value to prevent race conditions. Two goroutines access this configuration concurrently: the heartbeat loop (which may reload configuration from the server) and the main agent loop (which reads device lists and intervals). The atomic accessor pattern replaced a previous plain global variable that had race conditions detected by go test -race.

// ✅ CORRECT - Atomic configuration access
func startAgentLoop(ctx context.Context) {
    cfg := lib.GetConfiguration() // Atomic read returns snapshot
    sleepTime := time.Duration(cfg.Config.AgentUpdateInterval) * time.Second
    // ... use cfg ...
}

// ✅ CORRECT - Atomic configuration update
func fetchAgentConfig(ctx context.Context) error {
    fetchedCfg := lib.GetConfiguration() // Read current
    fetchedCfg.Config.UseNativeHashcat = true // Modify snapshot
    lib.SetConfiguration(fetchedCfg) // Atomic write
    return nil
}

// ❌ WRONG - Race condition with plain global variable (old pattern)
var Configuration agentConfiguration // REMOVED in PR #171
func oldPattern() {
    // Two goroutines reading/writing this simultaneously = data race
    interval := Configuration.Config.AgentUpdateInterval
}

Recommendation:

  • Use agentstate.State for agent operational settings (paths, timeouts, API credentials)
  • Use lib.GetConfiguration() for server-provided configuration (device lists, hashcat settings, update intervals)
  • Never access Viper directly outside of SetupSharedState and InitConfig

Configuration File Persistence#

CipherSwarmAgent occasionally needs to persist configuration changes back to disk, such as when discovering the Hashcat binary location at runtime.

Initial Config Creation with SafeWriteConfig#

During initialization in InitConfig(), the agent attempts to create a config file if none exists (lines 73-75):

if err := viper.ReadInConfig(); err == nil {
    agentstate.Logger.Info("Using config file", "config_file", viper.ConfigFileUsed())
} else {
    agentstate.Logger.Warn("No config file found, attempting to write a new one")

    if err := viper.SafeWriteConfig(); err != nil && err.Error() != "config file already exists" {
        agentstate.Logger.Error("Error writing config file", "error", err)
    }
}

Error handling pattern:

  • Use SafeWriteConfig() to avoid overwriting existing configs
  • Ignore "config file already exists" error (race condition benign)
  • Log other errors at ERROR level
  • Non-fatal: Agent continues even if write fails

Runtime Config Updates with WriteConfig#

When the agent discovers the Hashcat binary path at runtime, it persists this to config (lines 29-32):

agentstate.Logger.Info("Found Hashcat binary", "path", binPath)
agentstate.State.HashcatPath = binPath
viper.Set("hashcat_path", binPath)

if err := viper.WriteConfig(); err != nil {
    agentstate.Logger.Warn("Failed to persist hashcat path to config; path will be lost on restart",
        "error", err, "hashcat_path", binPath)
}

return nil

Error handling pattern:

  • Use WriteConfig() to update existing config file
  • Log failures at WARN level with descriptive message
  • Explain the consequence (value lost on restart)
  • Non-fatal: Function returns successfully even if write fails
  • Runtime state is still updated (agentstate.State.HashcatPath)

Non-Fatal Write Philosophy#

Both examples demonstrate CipherSwarmAgent's consistent philosophy of treating configuration write failures as non-fatal warnings. This design decision is appropriate for an agent that may run in:

  • Read-only filesystems
  • Container environments with ephemeral storage
  • Restricted permission environments
  • Airgapped labs with strict security policies

The agent prioritizes availability (continue running with correct runtime state) over persistence (ensure config survives restart).

Common Pitfalls and Best Practices#

Pitfall 1: Computing Derived Values in SetDefaultConfigValues#

Problem:

// ❌ WRONG
func SetDefaultConfigValues() {
    viper.SetDefault("data_path", "./data")
    viper.SetDefault("files_path", filepath.Join(viper.GetString("data_path"), "files"))
}

Why it fails: User overrides to data_path haven't been loaded yet; viper.GetString("data_path") returns only the default

Solution:

// ✅ CORRECT - In SetupSharedState() instead
func SetupSharedState() {
    dataPath := viper.GetString("data_path") // Now fully merged
    filesPath := viper.GetString("files_path")
    if filesPath == "" {
        filesPath = filepath.Join(dataPath, "files")
    }
    agentstate.State.FilesPath = filesPath
}

Pitfall 2: Accessing Configuration Before SetupSharedState#

Problem:

func someEarlyInit() {
    // May return incomplete/incorrect value
    fmt.Println(viper.GetString("data_path"))
}

Solution:

func properInit() {
    config.SetupSharedState() // Ensure config is complete
    // Safe to access after this point
    fmt.Println(agentstate.State.DataPath)
}

Pitfall 3: Forgetting to Validate Numeric Values#

Problem:

// May panic or use invalid value
retries := viper.GetInt("download_max_retries")

Solution:

// Validate in SetupSharedState with clamping
retries := viper.GetInt("download_max_retries")
if retries < 1 {
    agentstate.Logger.Warn("invalid retries, using default", "value", retries)
    retries = DefaultDownloadMaxRetries
}
agentstate.State.DownloadMaxRetries = retries

Pitfall 4: Treating Config Write Failures as Fatal#

Problem:

// ❌ WRONG - Kills agent on write failure
if err := viper.WriteConfig(); err != nil {
    return fmt.Errorf("fatal: %w", err)
}

Solution:

// ✅ CORRECT - Log and continue
if err := viper.WriteConfig(); err != nil {
    agentstate.Logger.Warn("config write failed; changes lost on restart", "error", err)
}
// Continue execution

Pitfall 5: Concurrent Access to Configuration Without Atomics#

Problem:

// ❌ WRONG - Race condition between goroutines
var Configuration agentConfiguration

func heartbeatLoop() {
    // Goroutine 1: reads configuration
    interval := Configuration.Config.AgentUpdateInterval
}

func reloadConfig() {
    // Goroutine 2: writes configuration
    Configuration = newConfig
}

Why it fails: Multiple goroutines reading/writing the same variable simultaneously causes data races detected by go test -race

Solution:

// ✅ CORRECT - Use atomic.Value for race-free access
var configuration atomic.Value

func init() {
    configuration.Store(agentConfiguration{})
}

func GetConfiguration() agentConfiguration {
    return configuration.Load().(agentConfiguration)
}

func SetConfiguration(cfg agentConfiguration) {
    configuration.Store(cfg)
}

func heartbeatLoop() {
    cfg := GetConfiguration() // Atomic read
    interval := cfg.Config.AgentUpdateInterval
}

func reloadConfig() {
    SetConfiguration(newConfig) // Atomic write
}

Best Practices Summary#

  1. Set simple defaults in SetDefaultConfigValues: Use viper.SetDefault() with literal values or exported constants
  2. Compute derived values in SetupSharedState: Never call viper.Get*() inside SetDefaultConfigValues
  3. Validate in SetupSharedState: All validation runs after config is fully merged
  4. Use atomic accessors for server configuration: Always call lib.GetConfiguration() and lib.SetConfiguration() for concurrent access
  5. Use agentstate.State for agent settings: Access operational settings (paths, timeouts, credentials) via agentstate.State
  6. Treat writes as non-fatal: Log errors but don't block execution
  7. Document defaults as constants: Maintain single source of truth in lib/config/config.go
  8. Test configuration precedence: Verify CLI flags override env vars, env vars override config files, etc.
  9. Test with race detector: Run go test -race to catch concurrent access bugs

Architecture Diagrams#

Initialization Sequence Diagram#

The following diagram illustrates the configuration initialization timing and data flow through the three phases:

Configuration Precedence Flow#

This diagram shows how configuration values flow through Viper's precedence hierarchy:

Deployment Considerations for Airgapped Environments#

CipherSwarmAgent's configuration architecture is specifically designed for deployment in airgapped, high-security laboratory environments where password cracking operations must run without Internet connectivity. These constraints significantly influence how configuration initialization is implemented.

Read-Only Filesystems#

Many secure environments mount application directories as read-only to prevent unauthorized modifications. The agent's non-fatal write error handling ensures continuous operation even when WriteConfig() fails. For example, when the agent discovers the Hashcat binary location and attempts to persist it to configuration, a write failure logs a warning but doesn't interrupt cracking operations. On the next restart, the agent simply rediscovers the binary location—a minor performance cost that ensures the agent never fails due to filesystem restrictions.

Container and Orchestrated Deployments#

Containerized deployments (Docker, Kubernetes) frequently use ephemeral storage where written configuration files are lost when containers restart. This pattern actually aligns well with the agent's architecture:

  • Environment variables for deployment-specific settings: API tokens, server URLs, and data paths can be injected via container environment variables
  • Mounted configuration files for static settings: GPU temperature thresholds, timeout values, and operational parameters can be provided as read-only mounted volumes
  • Runtime discovery over persistence: The agent rediscovers system resources (GPUs, Hashcat binary) on each startup rather than depending on persisted configuration
  • Validation at startup: SetupSharedState() catches configuration errors early, logging warnings for invalid values before cracking operations begin

This design ensures that containerized agents are stateless and can be scaled horizontally across GPU clusters without coordination.

Configuration Discovery and Graceful Degradation#

The agent performs runtime discovery of system resources to adapt to diverse hardware configurations:

Hashcat Binary Detection: The agent searches standard paths for native Hashcat installations. If found, it updates agentstate.State.HashcatPath immediately and attempts to persist this via WriteConfig(). If persistence fails (read-only filesystem, permission denied), the agent logs a warning but continues with the discovered path for the current session. On restart, the agent rediscovers the binary automatically.

GPU Device Enumeration: The agent detects available GPUs and validates against gpu_temp_threshold to protect hardware. These hardware characteristics are not persisted—they're discovered fresh on each startup to handle dynamic hardware changes in lab environments.

Network Connectivity: In airgapped environments, the agent must be pre-configured with the API URL of the CipherSwarm coordinator server within the isolated network. The configuration system validates that api_url is set during startup, failing fast if this required setting is missing rather than attempting to reach external services.

Example Deployment Patterns#

The following examples demonstrate how to configure CipherSwarmAgent for different airgapped deployment scenarios.

Environment Variable Configuration (Containerized Deployment):

# Docker/Kubernetes deployment - secrets via environment
export API_URL="https://cipherswarm.internal.lab"
export API_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
export DATA_PATH="/mnt/agent-data"
export GPU_TEMP_THRESHOLD="85"
./cipherswarm-agent

This approach is ideal for container orchestration where secrets are injected via platform mechanisms (Kubernetes secrets, Docker secrets) and configuration can be templated per deployment.

Config File Configuration (Bare Metal Deployment):

# /etc/cipherswarm/agent.yaml
api_url: https://cipherswarm.internal.lab
api_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
data_path: /opt/cipherswarm/data
gpu_temp_threshold: 85
task_timeout: 48h
download_max_retries: 5
./cipherswarm-agent --config /etc/cipherswarm/agent.yaml

This pattern suits traditional server deployments where configuration files can be version-controlled and distributed via configuration management tools (Ansible, Puppet, etc.).

Hybrid Configuration (Recommended for Production):

# /etc/cipherswarm/agent.yaml - Static operational settings
api_url: https://cipherswarm.internal.lab
gpu_temp_threshold: 85
task_timeout: 48h
heartbeat_interval: 15s
status_timer: 5
download_max_retries: 5
download_retry_delay: 3s
always_trust_files: false
enable_additional_hash_types: true
# Sensitive token and deployment-specific paths via environment and CLI
export API_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
./cipherswarm-agent \
  --config /etc/cipherswarm/agent.yaml \
  --data_path /mnt/raid/agent-data \
  --debug

Why this approach is recommended:

  • Security: Sensitive credentials never touch disk; they're provided via environment variables that can be managed by secret management systems
  • Flexibility: Static configuration (timeouts, thresholds) is version-controlled; deployment-specific settings (paths, debug flags) are provided at runtime
  • Precedence utilization: Leverages Viper's precedence model—CLI flags override environment variables override config files—allowing operators to temporarily override settings without editing files
  • Auditability: Config files show the intended operational parameters; environment variables and CLI flags show deployment-specific overrides

Real-World Lab Deployment Example:

A typical secure password recovery lab might deploy multiple agents across a GPU cluster:

# Node 1: High-performance cracking node with 8x RTX 4090 GPUs
export API_TOKEN="node1-token"
./cipherswarm-agent \
  --config /etc/cipherswarm/base-config.yaml \
  --data_path /nvme/agent-node1 \
  --gpu_temp_threshold 80 \
  --task_timeout 72h

# Node 2: General-purpose node with 4x RTX 3080 GPUs 
export API_TOKEN="node2-token"
./cipherswarm-agent \
  --config /etc/cipherswarm/base-config.yaml \
  --data_path /nvme/agent-node2 \
  --gpu_temp_threshold 75 \
  --task_timeout 24h

# Node 3: Development/testing node with single GPU
export API_TOKEN="dev-node-token"
./cipherswarm-agent \
  --config /etc/cipherswarm/base-config.yaml \
  --data_path /tmp/agent-dev \
  --debug \
  --extra_debugging \
  --force_benchmark_run

Each node shares a common base configuration file but uses environment variables and CLI flags for node-specific settings. This pattern scales to dozens of nodes while maintaining consistent operational parameters.

  • Agent State Management: The agentstate package and its role in centralizing runtime state
  • Logger Configuration: How the logger is initialized from configuration in initLogger()
  • Hashcat Integration: Configuration of native vs embedded Hashcat selection
  • Device Detection: GPU configuration and temperature threshold enforcement
  • Network Configuration: API client setup with timeout and retry configuration

Relevant Code Files#

FilePurposeKey Lines
cmd/root.goCLI command definition, flag binding, initialization sequence37-162 (init function)
lib/config/config.goDefault constants, SetDefaultConfigValues, SetupSharedState, InitConfig15-36 (constants), 79-189 (SetupSharedState), 191-217 (SetDefaultConfigValues)
lib/agent/agent.goAgent startup sequence, SetupSharedState invocation, concurrent config access42-54 (StartAgent), 237-308 (heartbeat/agent loops)
lib/agentClient.goAtomic configuration accessors (GetConfiguration, SetConfiguration)26-53 (atomic.Value initialization and accessors)
agentstate/agentstate.goShared state structure definition14-64 (State struct)
lib/crackerUtils.goRuntime config persistence example (Hashcat path)25-34 (setNativeHashcatPath)
lib/config/config_test.goConfiguration initialization test cases233-283 (derived path tests)
GOTCHAS.mdDocumented configuration pitfalls36-38 (Configuration section)

References#

Configuration Initialization Timing | Dosu