Cybersecurity-Projects/PROJECTS/intermediate/docker-security-audit/learn/00-OVERVIEW.md

26 KiB

Codebase Guide

A walkthrough of the code structure and what each package does. Start here if you want to understand how the pieces fit together.

Directory Layout

cmd/docksec/          Entry point and CLI commands
internal/
  analyzer/           Security checks for different target types
  benchmark/          CIS Docker Benchmark control definitions
  config/             Runtime configuration and constants# Docker Security Audit Tool (docksec)

What This Is

A Go-based CLI tool that scans Docker environments for security misconfigurations and validates them against the CIS Docker Benchmark. It analyzes running containers, daemon settings, images, Dockerfiles, and docker-compose files to identify vulnerabilities like privileged containers, dangerous capabilities, Docker socket mounts, and hardcoded secrets.

Why This Matters

Docker containers don't provide security by default. A single misconfiguration can give an attacker complete control over your host system. This tool catches those mistakes before they become breaches.

Real world scenarios where this applies:

  • Container escape via Docker socket mount: In 2018, Tesla's Kubernetes cluster was compromised when attackers found a pod with /var/run/docker.sock mounted, letting them escape to the host and mine cryptocurrency.

  • Privilege escalation through capabilities: The 2014 "Shocker" exploit used CAP_DAC_READ_SEARCH to read arbitrary files from the host filesystem, bypassing container isolation completely.

  • Secret exposure in images: In 2019, over 100,000 Docker images on Docker Hub contained embedded API keys, passwords, and private keys discoverable through simple text searches.

What You'll Learn

This project teaches you how container security actually works under the hood. By building and extending it, you'll understand:

Security Concepts:

  • CIS Docker Benchmark compliance - industry standard security controls covering host config, daemon settings, images, and runtime. You'll learn which of the 100+ controls matter most and why.
  • Linux capabilities model - how the 41 discrete privileges work, which ones enable container escape (like CAP_SYS_ADMIN and CAP_SYS_PTRACE), and how to audit them programmatically.
  • Namespace isolation boundaries - what PID, network, IPC, and mount namespaces protect against, and how sharing host namespaces breaks that protection.
  • Security profiles (seccomp, AppArmor, SELinux) - how syscall filtering and mandatory access control actually prevent attacks, not just theoretical defense-in-depth concepts.
  • Secret detection techniques - pattern matching with regex, entropy analysis using Shannon entropy, and why both are needed to catch different types of leaked credentials.

Technical Skills:

  • Docker API interaction - using the official Docker client library to introspect containers, images, and daemon configuration without shell commands.
  • Static analysis of build files - parsing Dockerfiles with AST (abstract syntax tree) to detect security issues at build time, before images ever run.
  • Concurrent scanning - using Go's errgroup and rate limiters to scan hundreds of containers efficiently without overwhelming the daemon.
  • Security reporting formats - generating SARIF (Static Analysis Results Interchange Format) for GitHub Security, JUnit for CI/CD, and structured JSON for automation.

Tools and Techniques:

  • moby/buildkit parser - the same parser Docker uses to understand Dockerfile syntax, giving you access to instruction metadata and line numbers for precise findings.
  • CIS Benchmark mapping - translating security controls into automated checks, including determining which findings are "scored" vs "not scored" in compliance reporting.
  • YAML node traversal - navigating docker-compose files as structured data rather than text, handling both mapping and sequence formats for the same logical configuration.

Prerequisites

Before starting, you should understand:

Required knowledge:

  • Go basics - structs, interfaces, goroutines, channels. You'll be reading and modifying Go code that uses errgroups for concurrency and rate limiters for API throttling.
  • Docker fundamentals - images vs containers, how volumes work, what ports do. You need to know the difference between docker run --privileged and --cap-add=NET_ADMIN.
  • Linux security model - what root can do, basic permission concepts, why running as UID 0 is dangerous. Understanding /proc and /sys helps but isn't required.
  • Command line proficiency - comfortable with docker inspect, reading JSON output, understanding exit codes and stderr vs stdout.

Tools you'll need:

  • Go 1.23+ - the project uses generics and newer error handling patterns
  • Docker Engine - running locally or accessible via DOCKER_HOST. Docker Desktop on Mac/Windows works fine.
  • Make (optional) - for running build and test commands conveniently
  • Git - to clone the repository and track your changes

Helpful but not required:

  • Experience with security scanning tools like Trivy, Snyk, or Anchore
  • Familiarity with the CIS Benchmark PDFs (the tool teaches you the controls)
  • Knowledge of Go testing and benchmarking

Quick Start

Get the project running locally:

# Clone and navigate
cd PROJECTS/intermediate/docker-security-audit

# Build the binary
go build -o docksec ./cmd/docksec

# Scan all running containers
./docksec scan

# Scan specific targets
./docksec scan --target containers
./docksec scan --target daemon
./docksec scan --target images

# Scan a Dockerfile
./docksec scan --file Dockerfile

# Scan a docker-compose file
./docksec scan --file docker-compose.yml

# Generate JSON output for automation
./docksec scan --output json --output-file results.json

# Generate SARIF for GitHub Security
./docksec scan --output sarif --output-file results.sarif

# Filter by severity
./docksec scan --severity high,critical

# Fail CI builds on findings
./docksec scan --fail-on medium

Expected output: Terminal report grouped by category (Container Runtime, Docker Daemon, etc.) showing findings with severity levels, CIS control IDs, descriptions, and remediation steps. Zero findings if your Docker environment is properly secured.

Project Structure

docker-security-audit/
├── cmd/
│   └── docksec/
│       └── main.go              # CLI entry point, Cobra commands
├── internal/
│   ├── analyzer/
│   │   ├── analyzer.go          # Analyzer interface
│   │   ├── container.go         # Running container checks
│   │   ├── daemon.go            # Docker daemon config checks
│   │   ├── image.go             # Image metadata checks
│   │   ├── dockerfile.go        # Dockerfile static analysis
│   │   └── compose.go           # docker-compose.yml checks
│   ├── benchmark/
│   │   └── controls.go          # CIS Docker Benchmark control registry
│   ├── config/
│   │   ├── config.go            # Configuration struct and filters
│   │   └── constants.go         # Rate limits, timeouts, thresholds
│   ├── docker/
│   │   └── client.go            # Docker API client wrapper
│   ├── finding/
│   │   └── finding.go           # Finding data model and collection
│   ├── proc/
│   │   ├── capabilities.go      # Linux capabilities parsing
│   │   ├── proc.go              # /proc filesystem reading
│   │   └── security.go          # Security profile inspection
│   ├── report/
│   │   ├── reporter.go          # Reporter interface and factory
│   │   ├── terminal.go          # Human-readable output
│   │   ├── json.go              # Structured JSON output
│   │   ├── sarif.go             # SARIF 2.1.0 format
│   │   └── junit.go             # JUnit XML for CI/CD
│   ├── rules/
│   │   ├── capabilities.go      # 41 Linux capabilities with risk levels
│   │   ├── paths.go             # 200+ sensitive host paths
│   │   └── secrets.go           # 80+ secret patterns and entropy
│   └── scanner/
│       └── scanner.go           # Main scan orchestration
├── Dockerfile                   # Multi-stage build for container deployment
├── go.mod                       # Dependencies
└── go.sum

Next Steps

  1. Understand the concepts - Read 01-CONCEPTS.md to learn how Linux capabilities, namespaces, and security profiles actually work at the kernel level.

  2. Study the architecture - Read 02-ARCHITECTURE.md to see how the scanner orchestrates concurrent analyzers, applies rules, and generates findings.

  3. Walk through the code - Read 03-IMPLEMENTATION.md for detailed implementation walkthroughs with actual code from the project.

  4. Extend the project - Read 04-CHALLENGES.md for specific ideas to add features like runtime monitoring, Kubernetes integration, and custom policies.

Common Issues

Docker daemon not accessible

Error: docker daemon not accessible: Cannot connect to the Docker daemon

Solution: Check that Docker is running (docker ps works), your user is in the docker group, or set DOCKER_HOST if using remote Docker.

No findings on known vulnerable containers Solution: Check your --severity and --target flags. The default scans all targets at all severities. Use --verbose to see which analyzers run.

Build fails with "module not found" Solution: Run go mod download to fetch dependencies. Make sure you're using Go 1.23 or later (go version).

SARIF file not showing in GitHub Security Solution: Upload it in a GitHub Actions workflow using github/codeql-action/upload-sarif@v2. SARIF files don't auto-upload, they need explicit CI integration.

If you found this interesting, check out:

  • Trivy - comprehensive vulnerability scanner that includes configuration scanning. This project focuses specifically on runtime and build-time Docker security.
  • Falco - runtime security monitoring using eBPF. Complements this tool by detecting threats during execution.
  • Docker Bench Security - official Docker security checker. This project improves on it with structured output, CI/CD integration, and programmatic access. docker/ Docker SDK wrapper finding/ The Finding type and severity levels parser/ Dockerfile and compose file parsers proc/ Linux /proc filesystem inspection report/ Output formatters (terminal, JSON, SARIF, JUnit) rules/ Security rule data (capabilities, paths, secrets) scanner/ Orchestration layer that ties everything together

## cmd/docksec

The CLI is built with Cobra. Each command is a separate file.

`main.go` defines version variables that get overwritten at build time:

```go
var (
    version   = "dev"
    commit    = "none"
    buildDate = "unknown"
)

When you run go build -ldflags "-X main.version=1.0.0", the compiler replaces the string literal before creating the binary.

scan.go is where the actual work starts. It creates a Config from flags, instantiates a Scanner, and calls Run:

cfg := &config.Config{
    Targets:  targets,
    Files:    files,
    Output:   outputFormat,
    // ...
}
scanner, _ := scanner.New(cfg)
scanner.Run(ctx)

internal/scanner

This is the orchestration layer. It creates analyzers based on config, runs them concurrently, collects findings, filters them, and sends to a reporter.

The concurrency model uses errgroup with a semaphore:

g, ctx := errgroup.WithContext(ctx)
g.SetLimit(s.cfg.Workers)  // Max concurrent goroutines

for _, a := range analyzers {
    a := a
    g.Go(func() error {
        s.limiter.Wait(ctx)  // Rate limit Docker API calls
        findings, _ := a.Analyze(ctx)
        results <- findings
        return nil
    })
}

The rate limiter prevents overwhelming the Docker daemon. Even if you set 50 workers, they spread their API calls over time.

internal/analyzer

Each analyzer implements this interface:

type Analyzer interface {
    Name() string
    Analyze(ctx context.Context) (finding.Collection, error)
}

There are five implementations:

File Target What it checks
container.go Running containers Privileged mode, capabilities, mounts, namespaces, security profiles, resource limits
image.go Local images User instruction, secrets in history, base image tags
daemon.go Docker daemon Insecure registries, ICC, user namespaces, experimental features
dockerfile.go Dockerfile files USER instruction, ADD vs COPY, secrets in ENV/ARG, HEALTHCHECK
compose.go Compose files Same as container checks, but for service definitions

Container analyzer is the most complex. It lists all containers, inspects each one, and runs checks:

func (a *ContainerAnalyzer) Analyze(ctx context.Context) (finding.Collection, error) {
    containers, _ := a.client.ListContainers(ctx, true)

    var findings finding.Collection
    for _, c := range containers {
        info, _ := a.client.InspectContainer(ctx, c.ID)
        findings = append(findings, a.analyzeContainer(info)...)
    }
    return findings, nil
}

func (a *ContainerAnalyzer) analyzeContainer(info types.ContainerJSON) finding.Collection {
    // Each method checks one thing
    findings = append(findings, a.checkPrivileged(target, info)...)
    findings = append(findings, a.checkCapabilities(target, info)...)
    findings = append(findings, a.checkMounts(target, info)...)
    // ...
}

Each check looks up the relevant CIS control for metadata:

func (a *ContainerAnalyzer) checkPrivileged(...) finding.Collection {
    if info.HostConfig.Privileged {
        control, _ := benchmark.Get("5.4")
        f := finding.New("CIS-5.4", control.Title, finding.SeverityCritical, target).
            WithDescription(control.Description).
            WithRemediation(control.Remediation)
        return finding.Collection{f}
    }
    return nil
}

internal/benchmark

Contains all CIS Docker Benchmark v1.6.0 controls as Go structs. They register themselves during init:

func init() {
    registerHostControls()
    registerDaemonControls()
    registerContainerRuntimeControls()
    // ...
}

func registerContainerRuntimeControls() {
    Register(Control{
        ID:          "5.4",
        Section:     "Container Runtime",
        Title:       "Ensure that privileged containers are not used",
        Severity:    finding.SeverityCritical,
        Description: "Privileged containers have all Linux kernel capabilities...",
        Remediation: "Do not run containers with --privileged flag...",
        Scored:      true,
        Level:       1,
    })
}

The global registry allows lookup by ID:

control, ok := benchmark.Get("5.4")

This keeps rule metadata in one place. If CIS updates the benchmark, you change the registry, not every analyzer.

internal/finding

The Finding struct carries everything about a discovered issue:

type Finding struct {
    ID          string       // Hash for deduplication
    RuleID      string       // CIS control ID
    Title       string       // Short description
    Description string       // Full explanation
    Severity    Severity     // INFO, LOW, MEDIUM, HIGH, CRITICAL
    Category    string       // Container Runtime, Dockerfile, etc.
    Target      Target       // What was scanned
    Location    *Location    // For files: path and line number
    Remediation string       // How to fix it
    References  []string     // Documentation links
    CISControl  *CISControl  // Original benchmark control
    Timestamp   time.Time    // When discovered
}

Findings are created with a builder pattern:

f := finding.New("CIS-5.4", "Privileged container", finding.SeverityCritical, target).
    WithDescription("...").
    WithRemediation("...").
    WithReferences("https://...")

The ID is generated from a hash of rule, target, and location. This makes findings stable across scans:

func (f *Finding) generateID() string {
    data := fmt.Sprintf("%s|%s|%s|%s", f.RuleID, f.Target.Type, f.Target.Name, f.Target.ID)
    hash := sha256.Sum256([]byte(data))
    return hex.EncodeToString(hash[:8])
}

Same issue on same target produces same ID. Useful for tracking remediation over time.

internal/rules

Contains security rule data organized by category:

capabilities.go

Maps all 40+ Linux capabilities to severity and descriptions:

var Capabilities = map[string]CapabilityInfo{
    "CAP_SYS_ADMIN": {
        Severity:    finding.SeverityCritical,
        Description: "Effectively root - mount filesystems, quotas, namespaces...",
    },
    "CAP_NET_RAW": {
        Severity:    finding.SeverityMedium,
        Description: "Craft arbitrary packets, ARP/DNS spoofing, packet sniffing.",
    },
    // ...
}

Pre computed lookup maps make checks fast:

var dangerousCapabilities = func() map[string]struct{} {
    m := make(map[string]struct{})
    for cap, info := range Capabilities {
        if info.Severity >= finding.SeverityHigh {
            m[cap] = struct{}{}
        }
    }
    return m
}()

func IsDangerousCapability(cap string) bool {
    _, exists := dangerousCapabilities[strings.ToUpper(cap)]
    return exists
}

paths.go

Catalogs 200+ sensitive host paths with severity and descriptions:

var SensitiveHostPaths = map[string]PathInfo{
    "/etc/shadow": {
        Severity:    finding.SeverityCritical,
        Description: "Password hashes. Direct credential access.",
    },
    "/var/run/docker.sock": {
        Severity:    finding.SeverityCritical,
        Description: "Docker daemon socket. Full control over Docker, container escape possible.",
    },
    // ...
}

Path matching handles prefixes:

func IsSensitivePath(path string) bool {
    if _, exists := sensitivePathLookup[path]; exists {
        return true
    }
    // Check if path is under a sensitive directory
    for sensitivePath := range sensitivePathLookup {
        if strings.HasPrefix(path, sensitivePath+"/") {
            return true
        }
    }
    return false
}

So /etc/shadow matches, but so does /etc/foo (because /etc is sensitive).

secrets.go

Pattern matching for secrets in Dockerfiles:

var SecretPatterns = []SecretPattern{
    {
        Type:        SecretTypeAWSKey,
        Pattern:     regexp.MustCompile(`(?i)(AKIA|ABIA|ACCA|ASIA)[0-9A-Z]{16}`),
        Description: "AWS Access Key ID",
    },
    {
        Type:        SecretTypeGitHub,
        Pattern:     regexp.MustCompile(`(?i)(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,255}`),
        Description: "GitHub Personal Access Token",
    },
    // 100+ more patterns
}

Also includes entropy calculation for detecting random strings that might be secrets:

func CalculateEntropy(s string) float64 {
    freq := make(map[rune]float64)
    for _, c := range s {
        freq[c]++
    }
    var entropy float64
    for _, count := range freq {
        p := count / float64(len(s))
        entropy -= p * math.Log2(p)
    }
    return entropy
}

High entropy strings (random characters) are likely secrets. Low entropy strings (repeated patterns) are not.

internal/parser

dockerfile.go

Parses Dockerfiles into an AST using BuildKit's parser:

func ParseDockerfile(path string) (*DockerfileAST, error) {
    file, _ := os.Open(path)
    result, _ := parser.Parse(file)

    ast := &DockerfileAST{
        Path: path,
        Root: result.AST,
    }
    ast.extractStructure()  // Build Commands and Stages slices
    return ast, nil
}

The AST provides helpers for common queries:

ast.HasInstruction("USER")           // Does it set a user?
ast.GetInstructions("ENV")           // All ENV instructions
ast.GetLastInstruction("USER")       // Last USER instruction
ast.FinalStage()                     // For multi-stage builds

compose.go

Parses docker-compose.yml files using the YAML library:

type ComposeFile struct {
    Path     string
    Services map[string]*Service
    Volumes  map[string]*Volume
    Networks map[string]*Network
}

type Service struct {
    Name        string
    Image       string
    Privileged  bool
    CapAdd      []string
    CapDrop     []string
    Volumes     []VolumeMount
    Ports       []PortMapping
    NetworkMode string
    // ...
}

Parsing handles both long and short syntax for volumes and ports:

# Short syntax
volumes:
  - ./host:/container

# Long syntax
volumes:
  - type: bind
    source: ./host
    target: /container

internal/docker

Wraps the Docker SDK with timeout handling:

type Client struct {
    api *client.Client
}

func (c *Client) InspectContainer(ctx context.Context, containerID string) (types.ContainerJSON, error) {
    // Create derived context with timeout
    inspectCtx, cancel := context.WithTimeout(ctx, config.InspectTimeout)
    defer cancel()

    return c.api.ContainerInspect(inspectCtx, containerID)
}

Uses a singleton pattern so the whole program shares one connection:

var (
    instance *Client
    once     sync.Once
)

func NewClient() (*Client, error) {
    once.Do(func() {
        cli, _ := client.NewClientWithOpts(
            client.FromEnv,
            client.WithAPIVersionNegotiation(),
        )
        instance = &Client{api: cli}
    })
    return instance, nil
}

internal/proc

Reads process information from the /proc filesystem. This works directly on the host kernel, bypassing Docker's abstractions.

func GetProcessInfo(pid int) (*ProcessInfo, error) {
    procPath := fmt.Sprintf("/proc/%d", pid)

    info := &ProcessInfo{PID: pid}
    info.readStatus(procPath)   // /proc/PID/status
    info.readCmdline(procPath)  // /proc/PID/cmdline
    info.readCgroups(procPath)  // /proc/PID/cgroup
    info.readNamespaces(procPath) // /proc/PID/ns/*
    return info, nil
}

Can detect if a process is in a container by checking cgroup paths:

func (p *ProcessInfo) IsInContainer() bool {
    for _, cg := range p.Cgroups {
        if strings.Contains(cg.Path, "docker") ||
           strings.Contains(cg.Path, "containerd") {
            return true
        }
    }
    return false
}

This package uses graceful degradation. If a file cannot be read (permissions, not Linux, etc.), it continues with what it can get rather than failing.

internal/report

Each output format implements the Reporter interface:

type Reporter interface {
    Report(findings finding.Collection) error
}

terminal.go

Colored output for interactive use:

func (r *TerminalReporter) Report(findings finding.Collection) error {
    for _, f := range findings {
        color := f.Severity.Color()  // ANSI escape code
        fmt.Printf("%s[%s]%s %s\n", color, f.Severity, reset, f.Title)
    }
    return nil
}

sarif.go

SARIF (Static Analysis Results Interchange Format) for GitHub Security tab:

type SARIFReport struct {
    Schema  string `json:"$schema"`
    Version string `json:"version"`
    Runs    []Run  `json:"runs"`
}

GitHub automatically picks up SARIF files and displays findings in the Security tab.

junit.go

JUnit XML for CI/CD integration:

type JUnitTestSuites struct {
    XMLName  xml.Name         `xml:"testsuites"`
    Tests    int              `xml:"tests,attr"`
    Failures int              `xml:"failures,attr"`
    Suites   []JUnitTestSuite `xml:"testsuite"`
}

Jenkins, GitLab CI, and others understand JUnit format for test reporting.

Adding a New Check

Say you want to add a check for containers using the latest tag.

  1. Find the relevant analyzer (container.go for running containers)

  2. Add a check method:

func (a *ContainerAnalyzer) checkImageTag(target finding.Target, info types.ContainerJSON) finding.Collection {
    if strings.HasSuffix(info.Config.Image, ":latest") || !strings.Contains(info.Config.Image, ":") {
        control, _ := benchmark.Get("5.27")
        f := finding.New("CIS-5.27", control.Title, finding.SeverityLow, target).
            WithDescription(control.Description)
        return finding.Collection{f}
    }
    return nil
}
  1. Call it from analyzeContainer:
findings = append(findings, a.checkImageTag(target, info)...)

The CIS control should already exist in benchmark/controls.go. If adding a custom check, register a new control first.

Adding a New Secret Pattern

In rules/secrets.go, add to the SecretPatterns slice:

{
    Type:        SecretTypeAPIKey,
    Pattern:     regexp.MustCompile(`myservice_[A-Za-z0-9]{32}`),
    Description: "MyService API Key",
},

The Dockerfile analyzer will automatically pick it up.

Adding a New Output Format

  1. Create a new file in internal/report/ (e.g., csv.go)

  2. Implement the Reporter interface:

type CSVReporter struct {
    output io.Writer
}

func (r *CSVReporter) Report(findings finding.Collection) error {
    w := csv.NewWriter(r.output)
    w.Write([]string{"ID", "Severity", "Title", "Target"})
    for _, f := range findings {
        w.Write([]string{f.ID, f.Severity.String(), f.Title, f.Target.String()})
    }
    return w.Flush()
}
  1. Update NewReporter in reporter.go to handle the new format:
case "csv":
    return &CSVReporter{output: out}, nil
  1. Add the format to CLI flag validation in cmd/docksec/scan.go

Testing Strategy

The codebase does not have extensive tests yet. If adding them:

Unit tests for rules packages (capabilities, paths, secrets):

func TestIsDangerousCapability(t *testing.T) {
    tests := []struct{cap string; want bool}{
        {"SYS_ADMIN", true},
        {"NET_BIND_SERVICE", false},
    }
    for _, tt := range tests {
        got := rules.IsDangerousCapability(tt.cap)
        if got != tt.want {
            t.Errorf("IsDangerousCapability(%q) = %v, want %v", tt.cap, got, tt.want)
        }
    }
}

Integration tests for analyzers using Docker SDK mocks or testcontainers.

E2E tests by running the binary against known bad configurations.