Security Assurance Case#
This document provides a structured argument that opnDossier meets its security requirements. It follows the assurance case model described in NIST IR 7608.
1. Security Requirements#
opnDossier is an OPNsense configuration parser, auditor, and reporting tool. Its security requirements are:
- SR-1: Must not crash or panic when processing any config.xml input
- SR-2: Must not allow XML External Entity (XXE) or entity expansion attacks
- SR-3: Must not allow path traversal via CLI arguments or output paths
- SR-4: Must not execute arbitrary code based on configuration file contents
- SR-5: Must not leak sensitive configuration data (passwords, keys, SNMP communities) in error messages or logs
- SR-6: Must not consume unbounded resources (memory, CPU) during parsing or report generation
- SR-7: Must handle sensitive data in generated reports with appropriate visibility controls
2. Threat Model#
2.1 Assets#
- Host system: The machine running opnDossier
- OPNsense/pfSense configuration: Contains network topology, firewall rules, credentials, VPN keys (including IPsec pre-shared keys), and other sensitive operational data
- Generated reports: May contain extracts of sensitive configuration data
2.2 Threat Actors#
| Actor | Motivation | Capability |
|---|---|---|
| Malicious config author | Exploit the parser to gain code execution or cause DoS | Can craft arbitrary config.xml content |
| Insider with report access | Extract sensitive data from generated reports | Can access report output files |
| Supply chain attacker | Compromise a dependency to inject malicious code | Can publish malicious Go module versions |
| Malicious plugin author | Stomp validator, exfiltrate credentials, inject backdoor via plugin init() before preflight | Can supply .so plugin files when --plugin-dir is enabled |
2.3 Attack Vectors#
| ID | Vector | Target SR |
|---|---|---|
| AV-1 | XXE or entity expansion in crafted config.xml | SR-2, SR-6 |
| AV-2 | Deeply nested XML elements cause stack overflow or resource exhaustion | SR-1, SR-6 |
| AV-3 | Extremely large config.xml causes memory exhaustion | SR-6 |
| AV-4 | CLI argument with path traversal writes reports to unintended locations | SR-3 |
| AV-5 | Crafted XML triggers panic in parser or schema mapping | SR-1 |
| AV-6 | Error messages include raw credential values from config | SR-5 |
| AV-7 | Compromised Go module introduces malicious code | SR-4 |
| AV-8 | Crafted IPsec XML elements exploit pfSense IPsec parser paths | SR-1, SR-2, SR-6 |
| AV-9 | Malicious plugin init() stomps validator during plugin.Open() load | SR-4 |
| AV-10 | OpenVPN TLS/StaticKeys HMAC keys leaked via sanitizer gap | SR-5 |
| AV-11 | Group/world-writable plugin file or directory exploited to swap plugin .so | SR-4 |
3. Trust Boundaries#
Dynamic plugins execute with full process privileges inside the opnDossier process when --plugin-dir is enabled (opt-in only). Plugin loading is preflight-hardened (symlink rejection, permission checks, SHA-256 audit log) but plugins are not sandboxed or signature-verified. All data crossing the trust boundary (config.xml content, CLI arguments, output paths, plugin .so files) is treated as untrusted and validated before use.
4. Secure Design Principles (Saltzer and Schroeder)#
| Principle | How Applied |
|---|---|
| Economy of mechanism | Go with minimal dependencies. Simple parser-schema-report pipeline. No plugin downloads, no scripting engine, no network I/O at runtime. |
| Fail-safe defaults | Both OPNsense and pfSense parsers (including IPsec configuration parsing) use shared pkg/parser/xmlutil.go (NewSecureXMLDecoder()) for secure XML decoding: entity expansion disabled, input size limited to 10 MB, charset normalization for UTF-8/ASCII/ISO-8859-1/Windows-1252. Overwrite protection on output files requires explicit --force flag. Offline-first design means no network calls. |
| Complete mediation | Every XML element is mapped to typed Go structs. Every CLI argument is validated by Cobra. Every output path is checked for overwrite conflicts. |
| Open design | Fully open source (Apache-2.0). Security does not depend on obscurity. All security mechanisms are publicly documented. |
| Separation of privilege | Parser, schema, audit, and export are separate packages with distinct responsibilities. Parse errors cannot bypass audit safety checks. |
| Least privilege | The tool reads config.xml files and writes reports; it never modifies source configurations or makes network connections. No elevated permissions required. Dynamic plugins (when --plugin-dir is enabled) run with full process privileges inside the opnDossier process, but loading is opt-in and requires explicit operator action. |
| Least common mechanism | No shared mutable state between report generations. Each invocation operates on its own parsed data. No global caches that could leak information between runs. |
| Psychological acceptability | CLI follows standard conventions via Cobra. Error messages are descriptive and actionable. Default behavior is safe (no overwrite, no network, offline-first). |
5. Common Weakness Countermeasures#
5.1 CWE/SANS Top 25#
| CWE | Weakness | Countermeasure | Status |
|---|---|---|---|
| CWE-787 | Out-of-bounds write | Go is memory-safe; slice/array access is bounds-checked at runtime. No unsafe package usage. | Mitigated |
| CWE-79 | XSS | Not applicable (no web output). Markdown reports are static files. | N/A |
| CWE-89 | SQL injection | Not applicable (no database). | N/A |
| CWE-416 | Use after free | Go's garbage collector prevents use-after-free. No manual memory management. | Mitigated |
| CWE-78 | OS command injection | No shell invocation or command execution. CLI arguments parsed by Cobra, not passed to shell. | Mitigated |
| CWE-20 | Improper input validation | All inputs validated: XML parsed by encoding/xml into typed structs, CLI args validated by Cobra, output paths checked for conflicts. | Mitigated |
| CWE-125 | Out-of-bounds read | Go slice access is bounds-checked at runtime. Panics on out-of-bounds access (caught by recovery or test suite). | Mitigated |
| CWE-22 | Path traversal | CLI accepts file paths as arguments. Output paths validated for overwrite protection. No path construction from config.xml contents. | Mitigated |
| CWE-352 | CSRF | Not applicable (no web interface). | N/A |
| CWE-434 | Unrestricted upload | Not applicable (no file upload). | N/A |
| CWE-476 | NULL pointer dereference | Go does not have null pointers in the C sense; nil pointer dereferences cause a recoverable panic. Pointer fields use nil checks or *string patterns with accessor methods. | Mitigated |
| CWE-190 | Integer overflow | Go integer arithmetic wraps silently but opnDossier performs no security-critical arithmetic. Linter (gosec G115) flags unsafe integer conversions. | Mitigated |
| CWE-502 | Deserialization of untrusted data | Config.xml is parsed by Go's encoding/xml into strictly typed structs, not arbitrary deserialization. | Mitigated |
| CWE-400 | Resource exhaustion | parser.NewSecureXMLDecoder() wraps input with io.LimitReader (10 MB default). Entity map cleared to prevent expansion. Both OPNsense and pfSense parsers share this hardening. | Mitigated |
| CWE-611 | XXE (XML External Entity) | parser.NewSecureXMLDecoder() sets dec.Entity = map[string]string{}, disabling all entity resolution. Go's encoding/xml does not support DTD processing. | Mitigated |
| CWE-312 | Cleartext storage of sensitive data | Credentials are redacted via two mechanisms, one unconditional and one opt-in. (1) sanitize (unconditional). The sanitize command uses field-pattern matching to redact credentials in configuration files (device-specific field names like pfSense <bcrypt-hash>, <pre-shared-key> and <ddnsdomainkey> require explicit patterns in internal/sanitizer/rules.go). OpenVPN TLS/StaticKeys HMAC keys (<tls> and <StaticKeys> elements) are detected via path-anchored patterns (openvpn.tls, openvpn.statickeys) and the IsOpenVPNStaticKey value detector to avoid false positives with non-OpenVPN <tls> elements in Suricata IDS and IPsec charon syslog configurations. Every sanitize mode redacts credentials; the mode selects how much network detail is additionally removed. (2) --redact (opt-in, default off). ToJSON/ToYAML take a redact parameter that convert, display and audit bind to the --redact flag. When set, redactSensitiveFields replaces certificate private keys (Certificate.PrivateKey), CA private keys (CertificateAuthority.PrivateKey), user API key secrets, SNMP community strings, WireGuard PSKs, the HA sync password and DHCPv6 key-info secrets with [REDACTED], using conditional deep-copy logic that only clones entries with non-empty sensitive values. Without --redact, JSON and YAML exports contain these values in cleartext, because CommonDevice carries them (see Public API); the markdown and HTML templates do not render key material in either case. Operators exporting JSON/YAML for sharing must pass --redact, or use sanitize on the config first. IPsec pre-shared keys are excluded from the common model entirely (json:"-" tag on pfsense.IPsecPhase1.PreSharedKey) and a conversion warning is emitted when a PSK is present, so they are never exposed on either path; the sanitizer also covers PSKs at the XML level via the "psk" substring pattern. See convert --redact and sanitize. | Partially mitigated (JSON/YAML requires --redact) |
| CWE-732 | Incorrect permission assignment | Dynamic plugin loader preflight (when --plugin-dir is enabled) rejects group/world-writable plugin files and directories via os.FileMode.Perm()&0o022 check (POSIX only). Symlinks are rejected via os.Lstat to prevent plugin.Open from following attacker-controlled links. Absolute paths are required so audit logs unambiguously identify loaded artifacts. Each load attempt produces a structured audit log (INFO for accepted, WARN for rejected) with fields: plugin name, path, SHA-256, mode, owner UID, mtime, size, verdict, reason. SHA-256 is computed with a 64 MiB cap to prevent memory exhaustion. On Windows, permission-bit checks are skipped (NTFS permissions do not map to POSIX mode bits) but symlink and absolute-path checks still run. Phase B follow-ups (owner UID check, configurable size cap, path denylist, filename allowlist, SHA-256 manifest, sandboxing) tracked for post-v1.5. See GOTCHAS Β§2.5. | Mitigated |
5.2 OWASP Top 10 (Where Applicable)#
Most OWASP Top 10 categories target web applications and are not applicable to a CLI configuration parser. The applicable items are:
| Category | Applicability | Countermeasure |
|---|---|---|
| A03: Injection | Partial -- XML parsing | Go's encoding/xml maps to typed structs; no dynamic query construction |
| A04: Insecure Design | Applicable | Secure design principles applied throughout (see Section 4) |
| A06: Vulnerable Components | Applicable | govulncheck and Trivy in CI, CodeQL via GitHub default-setup code scanning, Dependabot for automated updates, OSSF Scorecard |
| A09: Security Logging | Partial | Parse/audit errors logged via charmbracelet/log; security events reported via GitHub Advisories |
6. Supply Chain Security#
| Measure | Implementation |
|---|---|
| Dependency auditing | govulncheck (Go vulnerability database) and Trivy (filesystem scan + misconfiguration detection) run in CI via .github/workflows/security.yml on push/PR and weekly; CodeQL (semantic analysis) runs via GitHub's default-setup code scanning |
| Dependency updates | Dependabot configured for automated PRs |
| Pinned toolchain | Go version pinned via mise and go.mod |
| Reproducible builds | go.sum committed; CGO_ENABLED=0 static builds |
| Build provenance | Sigstore attestations via actions/attest-build-provenance |
| Artifact signing | Cosign keyless signing (Sigstore) + GPG signing via GoReleaser |
| SBOM generation | CycloneDX SBOM generated per release via cyclonedx-gomod |
| CI integrity | All GitHub Actions pinned to SHA hashes |
| Code review | Required on all PRs; automated by CodeRabbit with security-focused checks |
| License compliance | FOSSA scanning for Apache-2.0 compatible dependencies |
7. Ongoing Assurance#
This assurance case is maintained as a living document. It is updated when:
- New features introduce new attack surfaces
- New threat vectors are identified
- Dependencies change significantly
- Security incidents occur
The project maintains continuous assurance through two separately-managed scan streams:
- Workflow-based scans (
golangci-lint,govulncheck, Trivy) run from.github/workflows/security.ymlon every PR, every push tomain, and on a weekly schedule. - CodeQL runs via GitHub's repository-level default-setup code scanning. Its cadence and trigger set are managed by GitHub (not by a workflow in this repo) β typically on every push and PR, at a schedule GitHub controls. Advanced-setup CodeQL in a workflow would conflict with the default setup and is intentionally absent.