Security Model
DojOps implements defense-in-depth with seven layers between LLM output and infrastructure changes. No LLM output is trusted — every response crosses a trust boundary and is validated at every subsequent layer.
Defense-in-Depth Layers
LLM Response
|
+----v-----+
| Structured| Layer 1: Provider-native JSON mode
| Output | (OpenAI response_format, Anthropic prefill, Ollama format)
+----+------+
|
+----v-----+
| Input | Layer 2: Zod schema validation on every tool input
| Validation| and LLM response (parseAndValidate)
+----+------+
|
+----v------+
| Deep | Layer 3: External tool verification (on by default, --skip-verify):
|Verification| terraform validate, hadolint, kubectl dry-run
+----+------+
|
+----v-----+
| Policy | Layer 4: ExecutionPolicy controls write permissions,
| Engine | allowed/denied paths, env vars, timeouts, file size limits
+----+------+
|
+----v-----+
| Approval | Layer 5: ApprovalHandler with diff preview
| Workflow | (auto-approve, auto-deny, interactive callback)
+----+------+
|
+----v-----+
| Sandboxed | Layer 6: SandboxedFs restricts file operations
| Execution | to policy-allowed paths with per-file audit logging
+----+------+
|
+----v-----+
| Immutable | Layer 7: Hash-chained JSONL audit trail (SHA-256)
| Audit Log | with tamper detection via `dojops history verify`
+----------+Layer Details
Layer 1: Structured Output Enforcement
Every LLM request uses provider-native JSON modes to constrain output format:
| Provider | Mechanism |
|---|---|
| OpenAI | response_format: { type: "json_object" } |
| Anthropic | JSON prefill technique (assistant message prefix) |
| Ollama | format: "json" |
| DeepSeek | OpenAI-compatible response_format |
| Gemini | responseMimeType: "application/json" |
| GitHub Copilot | OpenAI-compatible response_format via Copilot API |
This prevents free-text responses and ensures the LLM produces parseable JSON.
Layer 2: Schema Validation
All LLM responses pass through parseAndValidate():
- Markdown stripping — Removes code fences that LLMs sometimes add
- JSON parsing —
JSON.parse()with error handling - Zod validation —
safeParse()against the expected schema
If validation fails, the response is rejected before any execution occurs. This applies to:
- Tool input schemas
- Tool output schemas
- TaskGraph schemas
- API request schemas
Layer 3: Deep Verification
External tool validation (enabled by default in CLI; --skip-verify to disable):
| Tool | Verifier | What It Checks |
|---|---|---|
| Terraform | terraform validate | HCL syntax, provider requirements, resource validity |
| Dockerfile | hadolint | Dockerfile best practices, security issues |
| Kubernetes | kubectl --dry-run=client | Manifest structure, API version compatibility |
| GitHub Actions | Structure lint (built-in) | on trigger, jobs section, runs-on, step validity |
| GitLab CI | Structure lint (built-in) | Job script fields, stages array, stage references |
Verification runs by default for CLI commands (apply, plan --execute). It gracefully skips if external tools are not installed. Failed verification blocks execution. The SDK default (skipVerification: true) remains unchanged for programmatic callers.
Layer 3b: DOPS Scope Enforcement
.dops modules can declare explicit write boundaries via the scope section:
scope:
write: ["{outputPath}/main.tf", "{outputPath}/variables.tf"]At file-write time, writeFiles() validates each resolved path against the expanded scope.write patterns. Writes to paths not covered by any scope pattern are rejected with an error. This provides tool-level write restriction independent of the global ExecutionPolicy, enabling defense-in-depth: even if the policy allows a broad set of paths, individual tools are constrained to their declared scope.
Path traversal (..) in scope patterns is rejected at parse time by the Zod schema validator.
Layer 4: Policy Engine
ExecutionPolicy provides fine-grained control:
- Write permissions —
allowWritemust betruefor any file operations - DevOps write allowlist — When no explicit
allowedPathsare set, only DevOps files (CI configs, Dockerfiles, Terraform, Kubernetes manifests, etc.) can be written. This prevents LLM-generated code from mutating application source files. Bypass with--allow-all-paths - Path allowlists —
allowedPathsrestricts where files can be written (takes precedence over DevOps allowlist) - Path denylists —
deniedPathsblocks specific paths (takes precedence over everything) - Environment isolation —
envVarscontrols available environment variables - Timeouts —
timeoutMsprevents runaway operations - Size limits —
maxFileSizeprevents oversized file writes
Layer 5: Approval Workflows
Before any write operation, the ApprovalHandler is invoked:
- Interactive mode — Shows a diff preview and prompts the user for confirmation
- Auto-approve — For
--yesflag and automated pipelines - Auto-deny — For
--dry-runand testing
The approval context includes file paths, content preview, and tool name.
Layer 6: Sandboxed Execution
SandboxedFs wraps all file operations:
- Path validation against the execution policy
- File size validation before writes
- Atomic write operations (temp-file +
fs.renameSync— POSIX atomic rename prevents partial writes on crash) - Per-file audit logging (path, size, timestamp, operation)
All 12 built-in tools and custom tools also use atomicWriteFileSync() from @dojops/sdk for direct file writes outside the sandbox.
Layer 7: Immutable Audit Log
Every operation produces a hash-chained audit entry:
- Append-only — Entries are appended to
.dojops/history/audit.jsonl(JSONL format, SIEM-compatible) - Hash chain — Each entry’s hash includes the previous entry’s hash (SHA-256)
- Tamper detection —
dojops history verifyrecomputes all hashes and detects any modifications - Structured entries — Each entry includes seq, timestamp, command, tool, status, verification result
- Random entry IDs —
HistoryStoreusescrypto.randomUUID()-based 12-char hex IDs instead of sequential counters, preventing ID prediction and reuse on restart
API Authentication
The REST API supports optional API key authentication:
- Key generation —
dojops serve credentialsgenerates acrypto.randomBytes(32)base64url key and saves it to~/.dojops/server.json(mode0o600) - Key loading — Server loads key from
DOJOPS_API_KEYenv var or~/.dojops/server.jsonat startup - Timing-safe comparison — All key comparisons use
crypto.timingSafeEqualto prevent timing attacks - Dual header support — Accepts
Authorization: Bearer <key>orX-API-Key: <key> - Health endpoint info-leak prevention — Unauthenticated callers to
/api/healthreceive only{ status, authRequired, timestamp }; full diagnostic payload (provider, tools, memory, uptime) requires authentication - autoApprove guard — The
POST /api/planendpoint’sautoApproveflag requires server authentication to be configured (403if no server key exists), preventing blind auto-approval via unauthenticated API calls - Dashboard login flow — The web dashboard stores the API key in
sessionStorage, injectsX-API-Keyheaders on all API calls, and shows a login overlay on 401/403 responses
Input Validation Hardening
Several API-layer input validation measures prevent abuse:
- Session ID format — Chat session IDs are validated against
/^chat-[a-f0-9]{8,16}$/with a.max(64)Zod constraint to prevent log injection and resource abuse - Log injection prevention — Session IDs in log output are sanitized (strip
\r\n\t, cap at 64 chars) to prevent log forging - Session hydration validation — When loading sessions from disk at startup, each session’s
idis validated againstisValidSessionId()to prevent corrupted data from entering memory - parseInt NaN guards — History route query parameters (
limit,offset) useNumber.isFinite()guards to preventNaNpropagation from malformed input - Scan timeout — Scan operations are wrapped in
Promise.racewith a configurable timeout (DOJOPS_SCAN_TIMEOUT_MS, default 120s) to prevent unbounded execution - MetricsAggregator bounds — File reads skip files >10MB, audit entries are capped at the last 10,000 lines, and
topIssuesis capped at 100 entries to prevent memory exhaustion - Atomic session persistence — Chat session writes use atomic file operations (write
.tmpthenfs.renameSync) to prevent partial writes on crash
Trust Boundary
UNTRUSTED TRUST BOUNDARY TRUSTED
+-----------+ +--------------------+ +-------------+
| LLM | -----> | Structured Output | -----> | Validated |
| Response | | + Schema Validation| | Tool Input |
+-----------+ +--------------------+ +-------------+LLM output is treated as untrusted external input. The trust boundary is at the Structured Output + Schema Validation layers. Only data that passes both layers enters the trusted execution pipeline.
Concurrency Safety
PID-based execution locking prevents concurrent mutations:
- Lock file —
.dojops/lock.jsoncontaining{ pid, command, timestamp } - Mutual exclusion — Only one
apply,destroy, orrollbackoperation at a time - Stale lock cleanup — If the locking PID is dead, the lock is automatically removed
- Exit code 4 — Returned when a lock conflict is detected
Security Scanning
Beyond the execution pipeline, DojOps provides proactive security scanning via @dojops/scanner:
- 9 scanners covering vulnerabilities, dependencies, IaC issues, secrets, shell scripts, SAST, and SBOM generation
- Exit codes 6 (HIGH) and 7 (CRITICAL) for CI/CD integration
- LLM-powered remediation with path-traversal protection
See Security Scanning for details.
Tool Isolation
Custom tools execute through the same SafeExecutor pipeline as built-in tools (inheriting maxFileSize, timeoutMs, DevOps write allowlist, and per-file audit logging), plus three additional security controls:
Verification Command Whitelist
The verification.command field in tool manifests can only invoke whitelisted binaries:
terraform, kubectl, helm, ansible-lint, docker, hadolint,
yamllint, jsonlint, shellcheck, tflint, kubeval, conftest,
checkov, trivy, kube-score, polarisAny other command (e.g., curl, rm, bash) is rejected at runtime with a descriptive error.
Permission Enforcement
The permissions.child_process field controls whether verification commands execute:
| Value | Behavior |
|---|---|
"required" | Command is checked against whitelist, then executed |
"none" | Command is never executed (skip) |
| (omitted) | Default-safe: command is never executed |
This ensures custom tools cannot execute shell commands unless they explicitly declare child_process: "required" AND the command is whitelisted.
Path Traversal Prevention
File paths in files[].path and detector.path are validated at schema level — any segment containing .. is rejected. This prevents custom tools from writing to or detecting files outside the project directory (e.g., ../../etc/passwd).
Tool Hash Integrity
Each custom tool has a SHA-256 hash computed from its tool.yaml content. This hash is pinned into plans at creation time and validated on --resume and --replay to detect tool modifications between plan creation and execution. See Tool Specification v1 for the full security model.
Replay Validation
dojops apply --replay enforces deterministic reproducibility by:
- Forcing
temperature: 0via aDeterministicProviderwrapper - Validating
providerandmodelmatch the plan’s execution context - Validating
systemPromptHashfor custom tool tasks to detect prompt drift
If any check fails, replay is aborted unless --yes forces continuation.
Plan Risk Classification
Plans are automatically classified into risk levels based on tool types and keyword analysis:
- LOW — CI/CD and monitoring tools (GitHub Actions, GitLab CI, Makefile, Prometheus)
- MEDIUM — Infrastructure tools (Terraform, Dockerfile, Kubernetes, Helm, Docker Compose, Ansible, Nginx, Systemd)
- HIGH — Any task mentioning IAM, security groups, production, secrets, credentials, RBAC, roles, or permissions
HIGH risk plans require explicit confirmation even with --yes (unless --force is also set), adding a safety gate before potentially dangerous operations.
DOPS-Declared Risk
.dops modules can also self-classify their risk level via the risk frontmatter section:
risk:
level: MEDIUM
rationale: "Infrastructure changes may affect cloud resources"This metadata is exposed via DopsRuntime.metadata.riskLevel and can be consumed by planners and approval workflows. The declared risk level complements the CLI’s keyword-based classifier — the CLI classifies plans, while .dops modules classify individual tools. When both are present, the higher risk level takes precedence.
Drift Awareness
Before execution, apply displays informational warnings for stateful tools (Terraform, Kubernetes, Helm, Ansible) reminding users that DojOps validates local config files but does not inspect remote state. Users are advised to run the appropriate tool-native drift check (e.g., terraform plan, kubectl diff) before applying.
Plan Snapshot Freezing
Plans capture an execution context snapshot at creation time:
- DojOps version — Detects version mismatches between plan creation and execution
- Policy hash — SHA-256 of the execution policy, detecting policy changes
- Tool versions — Per-tool version metadata for reproducibility
In --replay mode, version mismatches are blocking errors. In normal apply mode, mismatches produce informational warnings.
Best Practices
- Keep verification enabled (the default) in production for Terraform, Dockerfile, and Kubernetes tools. Only use
--skip-verifyfor speed during development - Review diffs before approving write operations in interactive mode
- Run
dojops history verifyperiodically to check audit trail integrity - Use
--dry-runto preview changes before applying - Restrict
allowedPathsin execution policies to limit blast radius - Run
dojops scanafter applying changes to check for introduced vulnerabilities