# AppArmor profile for the PawFlow relay container (pawflow-relay-dev).
#
# Replaces apparmor:unconfined on the relay. Unlike the pool profile
# (pawflow-mount), the relay runs arbitrary developer tooling, so `file`
# and `capability` stay broad — the value over unconfined is the
# docker-default /proc + /sys hardening plus blocking every mount EXCEPT
# the FUSE endpoints the relay legitimately creates:
#
#   * combined server-fs (pyfuse3) at /tmp/pf_combined_fs
#       — cc_sessions / filestore / skills are exposed from there as
#         symlinks (ln -s), NOT mounts, so they need no mount rule.
#   * rclone-backed conversation remote mounts (fuse.rclone) under /remote.
#
# Arbitrary binds (e.g. mount --bind /etc /x), overlay, proc remounts and
# root propagation changes all fall through to deny.
#
# STATUS: candidate, NOT yet wired into thread.py / docker-compose.yml.
# Validate on the host first:
#   sudo apparmor_parser -r -W docker/apparmor/pawflow-relay
#   sh scripts/test_apparmor_relay_profile.sh        # uses pawflow-relay-dev
# Only after a clean run AND a real relay starting with
# `--security-opt apparmor=pawflow-relay` mounting its combined-fs
# (look for "[FSRelay] combined-fs mounted") should it be wired in.

abi <abi/3.0>,

include <tunables/global>

profile pawflow-relay flags=(attach_disconnected,mediate_deleted) {
  include <abstractions/base>

  network,
  capability,
  file,
  umount,

  # libfuse char device.
  /dev/fuse rw,

  # fusermount3 / fusermount are setuid-root and perform the mount on
  # libfuse's behalf when the relay runs unprivileged. Let them run under
  # their own (Ubuntu-shipped) profile, or unconfined if none is loaded.
  # Px falls back to ix only if no attachment exists.
  /usr/bin/fusermount3 mrPUx,
  /usr/bin/fusermount  mrPUx,
  /bin/fusermount3     mrPUx,
  /bin/fusermount      mrPUx,

  # The one combined server-fs FUSE mount and anything under it.
  mount fstype=(fuse, fuse.*) -> /tmp/pf_combined_fs/,
  mount fstype=(fuse, fuse.*) -> /tmp/pf_combined_fs/**,

  # rclone conversation remote mounts.
  mount fstype=(fuse, fuse.*) -> /remote/,
  mount fstype=(fuse, fuse.*) -> /remote/**,

  # docker-default hardening, kept verbatim.
  deny @{PROC}/* w,
  deny @{PROC}/{[^1-9],[^1-9][^0-9],[^1-9s][^0-9y][^0-9s],[^1-9][^0-9][^0-9][^0-9/]*}/** w,
  deny @{PROC}/sys/[^k]** w,
  deny @{PROC}/sys/kernel/{?,??,[^s][^h][^m]**} w,
  deny @{PROC}/sysrq-trigger rwklx,
  deny @{PROC}/kcore rwklx,
  deny /sys/[^f]*/** wklx,
  deny /sys/f[^s]*/** wklx,
  deny /sys/fs/[^c]*/** wklx,
  deny /sys/fs/c[^g]*/** wklx,
  deny /sys/fs/cg[^r]*/** wklx,
  deny /sys/firmware/** rwklx,
  deny /sys/kernel/security/** rwklx,

  signal (send,receive) peer=pawflow-relay,
  ptrace (trace,read,tracedby,readby) peer=pawflow-relay,
}
