Tech Expert & Vibe Coder

With 14+ years of experience, I specialize in self-hosting, AI automation, and Vibe Coding – building applications using AI-powered tools like Google Antigravity, Dyad, and Cline. From homelabs to enterprise solutions.

Building a Docker Compose Security Audit System Inspired by Tailsnitch for Detecting Unauthorized Volume Mount Changes

Why I Built a Docker Compose Security Audit System

I run multiple Docker Compose stacks on my Proxmox homelab. Over time, I've accumulated services like n8n, monitoring tools, DNS resolvers, and various automation containers. Each stack has volume mounts—some for persistent data, others for configuration files, and a few that touch sensitive directories.

The problem: I had no visibility into when these mounts changed. Someone (usually me, weeks earlier) would add a new volume bind, and I'd forget why it existed. Worse, if a compose file got modified accidentally or maliciously, I wouldn't know until something broke or data leaked.

I needed a way to detect unauthorized volume mount changes before they became a problem. I'd heard about Tailsnitch, a tool that monitors Tailscale network changes and alerts you. The concept stuck with me: watch a configuration, compare it to a known baseline, and alert on drift.

I adapted that idea for Docker Compose files.

My Real Setup

I run Docker on a Debian 12 VM inside Proxmox. My compose files live in /opt/docker/, organized by service:

  • /opt/docker/n8n/docker-compose.yml
  • /opt/docker/monitoring/docker-compose.yml
  • /opt/docker/dns/docker-compose.yml

Each stack has volume mounts. Some are harmless (./data:/app/data), but others are sensitive:

  • /var/run/docker.sock:/var/run/docker.sock (full Docker API access)
  • /etc:/host-etc:ro (read-only host config access)
  • /home/vipin/.ssh:/root/.ssh (SSH keys, obviously dangerous)

I wanted to know immediately if any of these changed, especially the dangerous ones.

What I Built

I wrote a Python script that:

  1. Scans all docker-compose.yml files in /opt/docker/
  2. Extracts volume mounts from each service
  3. Compares them against a stored baseline (a JSON file)
  4. Alerts me via webhook if anything changed

The script runs as a systemd timer every hour. I use n8n to receive the webhook and send notifications to my phone via Pushover.

Core Logic

The script parses YAML using PyYAML, walks through each service's volumes list, and normalizes paths. It handles both short syntax (./data:/app/data) and long syntax (type: bind, source: /opt/data).

Here's the key part—I hash the entire volume configuration for each service. If the hash changes, I know something moved. I don't just check if volumes were added or removed; I also catch changes to mount options (like switching from :ro to :rw).

Baseline Storage

I store the baseline in /opt/docker/.volume-baseline.json. It looks like this:

{
  "n8n": {
    "n8n": [
      "/opt/docker/n8n/data:/home/node/.n8n",
      "/opt/docker/n8n/files:/files"
    ]
  },
  "monitoring": {
    "prometheus": [
      "/opt/docker/monitoring/prometheus:/prometheus"
    ]
  }
}

When I first ran the script, it created this baseline automatically. Every subsequent run compares against it.

Alert Mechanism

If a change is detected, the script sends a JSON payload to my n8n webhook:

{
  "alert": "volume_change",
  "stack": "n8n",
  "service": "n8n",
  "added": ["/var/run/docker.sock:/var/run/docker.sock"],
  "removed": [],
  "timestamp": "2025-01-15T14:32:00Z"
}

My n8n workflow parses this and sends a Pushover notification with the details. I get alerted within seconds.

What Worked

The script caught several issues I didn't expect:

  • I'd temporarily added /var/run/docker.sock to a container for debugging and forgot to remove it. The alert reminded me.
  • A compose file I copied from GitHub included /etc:/host-etc:ro. I hadn't noticed. The alert flagged it immediately.
  • I changed a mount from :ro to :rw while testing. The script caught the permission change, even though the path stayed the same.

The systemd timer approach worked well. I run it hourly, which is frequent enough to catch changes quickly but not so often that it causes noise.

Using n8n for alerting was the right choice. I already had it running, and the webhook integration took five minutes to set up. I can easily extend it later (e.g., log to a database, send to Slack, etc.).

What Didn't Work

My first version only checked if volumes were added or removed. It missed permission changes (:ro vs :rw). I had to rewrite the comparison logic to hash the full mount string, including options.

I initially stored the baseline in the same directory as each compose file. This broke when I used Git to manage my compose files—the baseline got overwritten on every pull. I moved it to a single global file outside version control.

The script doesn't handle named volumes well. It only tracks bind mounts (host paths). Named volumes are managed by Docker and don't pose the same security risk, but I still want to monitor them eventually. For now, I just skip them.

I tried using Docker's events API to detect changes in real-time, but it doesn't fire when you edit a compose file—only when you run docker-compose up. I needed something that caught changes before deployment, so I stuck with periodic file scanning.

How It Runs

I created a systemd service and timer:

/etc/systemd/system/docker-volume-audit.service:

[Unit]
Description=Docker Compose Volume Audit
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /opt/scripts/docker-volume-audit.py
User=root

/etc/systemd/system/docker-volume-audit.timer:

[Unit]
Description=Run Docker Volume Audit Hourly

[Timer]
OnCalendar=hourly
Persistent=true

[Install]
WantedBy=timers.target

I enabled it with:

sudo systemctl enable --now docker-volume-audit.timer

It runs at the top of every hour. I can also trigger it manually with:

sudo systemctl start docker-volume-audit.service

Limitations I Accepted

This is not a real-time security monitor. If someone modifies a compose file and immediately runs docker-compose up, the script won't catch it until the next hourly check. I considered using inotify to watch for file changes, but that felt like overkill for my setup.

The script only checks compose files. If someone runs docker run directly with a dangerous volume mount, I won't know. I accept this because I don't use docker run for anything persistent—everything goes through compose.

It doesn't validate whether a mount is actually dangerous. It just reports changes. I have to decide if each alert matters. I thought about adding a whitelist for known-safe mounts, but I prefer seeing everything and making the call myself.

Key Takeaways

  • Monitoring configuration drift is more useful than I expected. Even small changes can matter.
  • Periodic scanning (hourly) is good enough for my threat model. Real-time monitoring would add complexity without much benefit.
  • Using existing tools (n8n, Pushover) for alerting was faster than building something custom.
  • Storing the baseline outside version control prevents accidental overwrites.
  • Hashing the full mount string (including options) catches permission changes, not just path changes.

This system doesn't replace proper access controls or Docker security hardening. It's just another layer—one that tells me when something unexpected happens. That's all I needed.