Documentation
¶
Overview ¶
Package selfaudit scores a running Redoubt installation against its own secure-by-default checklist, in the spirit of docker-bench-security (E2.7): daemon configuration, the compose bundle (socket-proxy, Traefik, platformd, buildkitd), every platform-managed container, the control-plane's own state (Owner TOTP, bootstrap token, key file mode, audit chain) and, when it runs on the host, a few daemon files.
The package has two halves. Evaluate is pure: it takes typed Inputs (a docker system.Info, the inspect results of every container, counts from the store, host file facts) and produces a Report, so the checks are unit-tested against fixture inspect JSON in testdata/. Collector gathers those Inputs from the live system using only read-only, allow-listed Docker calls (GET /info, GET /containers/json, GET /containers/{id}/json — TestSelfAuditReadOnly pins it) and store reads; it never creates, changes or removes anything.
Index ¶
Constants ¶
const ( SeverityFail = "fail" // a golden-rule or plan-mandated control is off SeverityWarn = "warn" // a recommended hardening is missing SeverityInfo = "info" // informational only )
Severity of a check that does not pass.
const ( StatusPass = "pass" StatusWarn = "warn" StatusFail = "fail" StatusSkip = "skip" // could not be evaluated here (inside the container, missing input) )
Status of one check.
const ( BundlePlatformd = "redoubt-platformd" BundleSocketProxy = "redoubt-socket-proxy" BundleBuildkitd = "redoubt-buildkitd" )
Compose-bundle container names (install/compose/docker-compose.yml); Traefik's comes from config.Config.TraefikContainer.
const ( IDDaemonLiveRestore = "daemon.live-restore" IDDaemonUserlandProxy = "daemon.userland-proxy" IDDaemonNNP = "daemon.no-new-privileges" IDDaemonSeccomp = "daemon.seccomp" IDDaemonAppArmor = "daemon.apparmor" IDBundlePresent = "bundle.present" IDContainersHardened = "containers.hardened" IDContainersLimits = "containers.limits" IDContainersConfined = "containers.confined" IDSocketOnlyProxy = "socket.only-proxy" IDSocketProxyNoPorts = "socket-proxy.no-ports" IDPlatformdLoopback = "platformd.loopback-only" IDTraefikNoDocker = "traefik.no-docker-provider" IDTraefikACME = "traefik.acme-resolver" IDBuildkitdNonRoot = "buildkitd.non-root" IDAuthOwnerTOTP = "auth.owner-totp" IDAuthBootstrapToken = "auth.bootstrap-token" // #nosec G101 -- check id, not a credential IDSecretsKeyMode = "secrets.age-key-mode" IDAuditChain = "audit.chain" )
Check IDs, stable for scripting and for the tests.
const ActionRun = "selfaudit.run"
ActionRun is the audit action recorded per run.
const DaemonJSONPath = "/etc/docker/daemon.json"
DaemonJSONPath is the daemon configuration read when the audit runs on the host.
const PermAuditRead = string(auth.PermAuditRead)
PermAuditRead mirrors auth.PermAuditRead: reading the self-audit is reading the security posture, which is the audit reader's job.
Variables ¶
var ErrForbidden = errors.New("selfaudit: forbidden")
ErrForbidden is returned when the actor may not read the audit.
Functions ¶
func InContainer ¶
func InContainer() bool
InContainer reports whether the process runs inside a container (Docker creates /.dockerenv).
Types ¶
type Actor ¶
Actor is who requests the audit. Can nil means every permission (the bootstrap principal).
type ChainResult ¶
type ChainResult struct {
// Checked is false when no verifier was wired.
Checked bool
OK bool
Detail string
Err error
}
ChainResult is what the audit chain verifier reported.
type ChainVerifier ¶
ChainVerifier walks the hash-chained audit log. It is satisfied by the audit package's chain (E2.1); platformd adapts whatever that exposes. ok is false when the chain is broken, with detail naming the first bad row; err is for failures to run the check at all.
type Check ¶
type Check struct {
ID string `json:"id"`
Title string `json:"title"`
Severity string `json:"severity"`
Status string `json:"status"`
Detail string `json:"detail"`
}
Check is one line of the report.
type Collector ¶
type Collector struct {
Docker *docker.Client
Store *store.Store
Cfg config.Config
// Chain is optional; nil skips the audit-chain check.
Chain ChainVerifier
// InContainer marks a run inside the platformd container (host-only checks are skipped).
InContainer bool
// DaemonJSONPath overrides DaemonJSONPath (tests).
DaemonJSONPath string
// Now is overridable for tests.
Now func() time.Time
}
Collector gathers Inputs from the live system. Every Docker call it makes is a GET on the allow-list (info, containers/json, containers/{id}/json); it performs no writes anywhere.
type DataFiles ¶
type DataFiles struct {
AgeKeyMode fs.FileMode
AgeKeyExists bool
AgeKeyErr error
// BootstrapTokenExists is true when <data>/bootstrap-token is present.
BootstrapTokenExists bool
BootstrapTokenErr error
}
DataFiles are facts about files under the data directory.
type HostFacts ¶
type HostFacts struct {
// DaemonJSON is the parsed /etc/docker/daemon.json; nil when the file does not exist.
DaemonJSON map[string]any
// DaemonJSONErr is set when the file exists but could not be read or parsed.
DaemonJSONErr error
}
HostFacts are read from the host's filesystem (never inside the container).
type Inputs ¶
type Inputs struct {
Info system.Info
// Bundle maps compose-bundle container names to their inspect result; a name that is absent
// (or mapped to nil) was not found.
Bundle map[string]*container.InspectResponse
// Managed are the containers labelled redoubt.managed=true: apps, addons, one-shot jobs.
Managed []container.InspectResponse
Cfg config.Config
// InContainer is true when the audit runs inside the platformd container; host-only checks
// are then skipped instead of reported.
InContainer bool
// Host holds the daemon file facts gathered on the host (nil inside the container).
Host *HostFacts
// Files holds facts about files under the data directory (readable from both sides).
Files DataFiles
// UserCount is the number of user accounts; OwnerTOTP is true when at least one enabled
// Owner has a confirmed second factor.
UserCount int64
OwnerTOTP bool
// Chain is the audit-chain verification outcome.
Chain ChainResult
}
Inputs is everything Evaluate looks at. Collector fills it from the live system; tests fill it from fixtures.
type Report ¶
type Report struct {
// Score is 0..100: passes / (passes + fails + 0.5*warns), skips excluded.
Score int `json:"score"`
Checks []Check `json:"checks"`
GeneratedAt time.Time `json:"generated_at"`
}
Report is the outcome of a self-audit run.