name: cyber-decoy services: broker: build: context: ./broker container_name: decoy-broker restart: unless-stopped # eBPF TC attachment needs these capabilities plus a mounted BPF fs. # Prefer scoped capabilities over full privileged mode where the host # kernel allows it (see README for the privileged fallback). cap_add: - NET_ADMIN - BPF - PERFMON security_opt: - apparmor=unconfined ports: - "22:22" - "3389:3389" - "445:445" networks: - edge - decoynet depends_on: - ssh-decoy - rdp-decoy - smb-decoy # Each decoy is self-contained, so its build context is its own directory. ssh-decoy: build: context: ./decoys/ssh container_name: decoy-ssh restart: unless-stopped networks: - decoynet volumes: # Persist the generated SSH host key. Without this, the fingerprint # changes on every restart, which is a giveaway that the host is fake. - ssh-keys:/var/lib/opencanary rdp-decoy: build: context: ./decoys/rdp container_name: decoy-rdp restart: unless-stopped networks: - decoynet smb-decoy: build: context: ./decoys/smb container_name: decoy-smb restart: unless-stopped networks: - decoynet # Single non-root Python process (Impacket SimpleSMBServer). It only needs # to bind the privileged port 445; everything else is dropped. Read-only # root filesystem since the decoy writes nothing. Note: Impacket's smbserver # carried CVE-2021-31800 (path traversal) which specifically hit honeypots, # so this hardening is deliberate, not boilerplate. requirements.txt pins a # fixed release; keep it current. read_only: true security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - NET_BIND_SERVICE networks: # Public-facing network where the broker publishes ports to the host. edge: driver: bridge # Internal network. Decoys are reachable only by the broker, never the host. decoynet: driver: bridge internal: true volumes: ssh-keys: