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.

Configuring Portainer with Physical Security Controls: Detecting USB Device Passthrough Attacks in Container Environments

Why I Worked on This

I run Portainer on a Proxmox node to manage Docker containers across my home lab. Most of my workloads are isolated by design—services don't need direct hardware access. But I started thinking about what happens when someone (or something) deliberately passes a USB device into a container.

The trigger was simple: I wanted to understand if an attacker with access to Portainer could silently attach a USB device to a running container and exfiltrate data or inject malicious firmware. Not because I'm paranoid, but because I treat my infrastructure like it could be compromised at any time.

This isn't about theoretical exploits. I wanted to know if I could detect this kind of activity in my own environment, using tools I already have running.

My Real Setup

My Portainer instance runs as a container on a dedicated Proxmox VM. The VM has Docker installed, and Portainer manages about 15 containers—mostly web services, automation tools, and monitoring stacks.

I don't use Portainer's Business Edition. I'm on the Community Edition, which means I don't have advanced RBAC or audit logging features. What I do have:

  • Access to Docker's event stream
  • A logging stack (Loki + Promtail)
  • Basic alerting through n8n
  • Physical access controls on the Proxmox host itself

The Proxmox node has USB controllers, and I've passed USB devices through to VMs before (like a Zigbee stick for Home Assistant). But I've never intentionally passed a USB device into a Docker container through Portainer.

How USB Passthrough Actually Works in Portainer

Portainer lets you add devices to containers through the "Runtime & Resources" section under Advanced settings. You specify a host path (like /dev/bus/usb/001/002) and a container path where the device should appear.

This uses Docker's --device flag under the hood. It's not magic—it's just mounting a device node from the host into the container's filesystem. The container then has direct access to that hardware.

Here's what I tested:

  1. I plugged a USB flash drive into the Proxmox host
  2. I identified it with lsusb and ls /dev/bus/usb/
  3. I created a test container in Portainer
  4. I added the USB device through the Devices field in Runtime settings
  5. I started the container and confirmed the device was accessible inside

It worked. The container could read from the USB drive. No special permissions needed beyond having access to Portainer's UI.

The Security Problem

If someone has access to Portainer (even read-write access to a single stack), they can:

  • Edit an existing container
  • Add a USB device mapping
  • Restart the container
  • Access the device from inside the container

This bypasses most network-based security controls. The data path goes directly from hardware to container. No firewall rules, no network segmentation, no TLS inspection.

Even worse: if the USB device is something like a BadUSB or a malicious storage device with firmware exploits, the container could potentially compromise the host.

What I Built to Detect This

I didn't find a built-in way to monitor device passthrough events in Portainer. So I went to Docker's event stream.

Docker emits events for container lifecycle actions—create, start, stop, die, etc. These events include details about the container's configuration, including device mappings.

I set up a simple monitoring flow:

  1. Stream Docker events to a log file
  2. Parse the events for container actions
  3. Extract device mappings from the event payload
  4. Alert if a new device is added to a running container

Docker Event Streaming

I used Docker's event API directly. You can stream events with:

docker events --filter 'type=container' --format '{{json .}}'

This outputs JSON for every container event. The relevant fields are:

  • Action: What happened (create, start, update, etc.)
  • Actor.Attributes: Contains configuration details
  • Actor.ID: The container ID

Device mappings appear in the attributes, but they're not always present in every event type. I had to test which actions actually include device information.

What I found: create and update events include device mappings. start events do not.

Parsing and Alerting

I wrote a small Python script that:

  1. Connects to Docker's event stream
  2. Filters for create and update actions
  3. Checks if the Actor.Attributes contains any keys starting with device
  4. Logs the event with container name, device path, and timestamp
  5. Sends a webhook to n8n if a device is detected

The script runs as a systemd service on the Proxmox host. It's not inside a container because I want it to survive even if Docker is compromised.

Here's the core logic (simplified):

import docker
import requests

client = docker.from_env()

for event in client.events(decode=True, filters={'type': 'container'}):
    action = event.get('Action')
    if action in ['create', 'update']:
        attrs = event.get('Actor', {}).get('Attributes', {})
        devices = {k: v for k, v in attrs.items() if k.startswith('device')}
        
        if devices:
            container_name = attrs.get('name', 'unknown')
            log_entry = {
                'container': container_name,
                'action': action,
                'devices': devices,
                'timestamp': event.get('time')
            }
            
            # Log to file
            print(log_entry)
            
            # Send alert
            requests.post('http://n8n.local/webhook/usb-alert', json=log_entry)

This is not production-grade code. It doesn't handle reconnections, it doesn't validate webhook responses, and it doesn't deal with high event volumes. But it works for my setup.

Alert Flow in n8n

The n8n workflow receives the webhook and:

  1. Checks if the device path is in an allowlist (I have a few known devices)
  2. If not, sends a Telegram message to me
  3. Logs the event to a dedicated Loki stream for audit purposes

I don't automatically block the container because false positives are annoying. Instead, I get notified and can investigate manually.

What Worked

The detection works. When I add a USB device through Portainer, I get an alert within seconds. The log includes the container name, the device path, and the action type.

I tested it with:

  • A USB flash drive
  • A USB serial adapter
  • A webcam

All were detected correctly. The false positive rate is low because I rarely add devices to containers.

Why This Approach Works

Docker's event stream is reliable. It's a core part of Docker's architecture, and it's been stable across versions. Unlike log scraping or API polling, event streaming is real-time and doesn't miss actions.

The script is simple enough that I can audit it in five minutes. No dependencies beyond the Docker SDK. No complex parsing logic. Just filtering and forwarding.

What Didn't Work

I initially tried to use Portainer's own logs to detect device changes. Portainer logs API calls, but the logs don't include detailed configuration changes. They show that a container was updated, but not what changed.

I also tried using Docker's inspect command to periodically check container configurations. This works, but it's slow and doesn't scale. Polling every container every few seconds is wasteful, and you can miss changes if they happen between polls.

Another failed approach: I tried to use Linux audit logs (auditd) to monitor /dev/bus/usb access. This generated too much noise. Every time a legitimate process scanned USB devices, I got an event. Filtering was painful and error-prone.

Limitations of This Setup

This detection method only works if:

  • The attacker uses Portainer (or Docker CLI) to add the device
  • The monitoring script is running and has access to Docker's socket
  • The attacker doesn't have root access to the host

If someone has root on the Proxmox host, they can bypass Docker entirely and access USB devices directly. They could also kill the monitoring script or tamper with the event stream.

This is not a complete solution. It's a layer of detection, not prevention.

Physical Security Still Matters

The bigger lesson here is that physical access to the host is game over. If someone can plug a USB device into the Proxmox node, they don't need to go through Portainer. They can:

  • Boot from a USB stick and access the filesystem
  • Use a BadUSB device to inject commands
  • Install a hardware keylogger

My detection setup only catches attacks that use the Docker API. It doesn't protect against physical tampering.

For my home lab, that's acceptable. The Proxmox host is in a locked room, and I'm the only one with physical access. But if this were a production environment, I'd need:

  • Locked server racks
  • USB port blockers or disabled USB controllers
  • BIOS-level boot restrictions
  • Intrusion detection on the physical enclosure

None of that is relevant to my current setup, so I didn't implement it.

Key Takeaways

Docker's device passthrough is powerful but dangerous. It gives containers direct hardware access, which breaks most isolation assumptions.

Portainer makes it easy to configure device passthrough, but it doesn't log or audit these changes in a way that's useful for security monitoring.

Docker's event stream is the best place to detect configuration changes. It's real-time, reliable, and doesn't require polling.

Detection is not prevention. If someone has access to Portainer, they can do a lot of damage before you notice. The real security control is limiting who has access in the first place.

Physical security is still the foundation. All the monitoring in the world won't help if someone can walk up to the server and plug in a USB device.

For my setup, this level of monitoring is enough. I get alerted when something unusual happens, and I can investigate. But I'm not defending against nation-state actors—I'm just trying to catch mistakes and opportunistic attacks.