# Tighten a world-readable compose `.env` that holds secrets to 0600.
#
# WHY: found 2026-08-23 on ana-docker. Eight stacks kept secret-bearing .env
# files at mode 0644 — readable by every local account on the box (verified by
# reading one as `nobody`; the host has four interactive users). Six other
# stacks already used 0600, so this is converging on the existing house
# pattern rather than inventing one.
#
# Usage — one run per stack:
#   scripts/elway ana-docker --playbook playbooks/tighten-env-perms.yaml \
#     --var stack=vaultwarden
#
# SAFE BECAUSE, verified before writing this:
#   - every target .env is owned by lkraven, and lkraven is the deploy user,
#     so 0600 preserves the deploy path
#   - none of them is bind-mounted INTO a container. They are consumed either
#     by `env_file:` or by `${VAR}` interpolation, both of which docker
#     compose reads at deploy time as the invoking user. A .env that WERE
#     bind-mounted would be read by the container's own UID and 0600 could
#     break it — check for that before adding a stack to this sweep.
#   - chmod does not touch a running container; env is injected at create.
#
# The playbook re-checks ownership itself and refuses if it is not lkraven,
# so a stack that does not fit the above cannot be swept in by accident.

vars:
  stack: ""
  compose_root: /opt/docker/compose
  expect_owner: lkraven

steps:
  - name: Refuse to run without an explicit stack
    shell: test -n "{{ stack }}"
    changed_when: "false"

  - name: Target .env exists
    sudo: true
    shell: test -f {{ compose_root }}/{{ stack }}/.env
    changed_when: "false"

  - name: Owner is the deploy user (else 0600 would break deploys)
    sudo: true
    shell: |
      own=$(stat -c %U {{ compose_root }}/{{ stack }}/.env)
      test "$own" = "{{ expect_owner }}" || {
        echo "REFUSING: .env is owned by $own, not {{ expect_owner }} — 0600 would lock the deploy user out"
        exit 1; }
      echo "owner ok: $own"
    changed_when: "false"

  - name: Not bind-mounted into a container (that would be read by the container UID)
    sudo: true
    # Greps the raw inspect JSON rather than using a Go range template:
    # elway's variable regex matches any bare identifier in braces, so
    # `{{end}}` and `{{println}}` get eaten as undefined variables. Anything
    # starting with a dot (`{{.Source}}`) or containing a space
    # (`{{json .Mounts}}`) passes through, but plain grep avoids the whole
    # class of trap.
    shell: |
      if docker inspect {{ stack }} 2>/dev/null | grep -qE '"Source": *"[^"]*/\.env"'; then
        echo "REFUSING: {{ stack }} bind-mounts its .env; 0600 may break the container"
        exit 1
      fi
      echo "no .env bind mount"
    changed_when: "false"

  - name: Tighten to 0600
    sudo: true
    # Skipped when already 0600, so a re-run reports OK instead of a phantom
    # CHANGED and the sweep is safe to run repeatedly.
    when: "test \"$(sudo stat -c %a {{ compose_root }}/{{ stack }}/.env)\" != \"600\""
    shell: |
      before=$(stat -c %a {{ compose_root }}/{{ stack }}/.env)
      chmod 600 {{ compose_root }}/{{ stack }}/.env
      echo "{{ stack }}: $before -> 600"
    changed_when: "true"

verify:
  - name: Mode is 0600 and the file is no longer world-readable
    sudo: true
    shell: |
      m=$(stat -c %a {{ compose_root }}/{{ stack }}/.env)
      test "$m" = "600" || { echo "mode is $m, expected 600"; exit 1; }
      if sudo -u nobody test -r {{ compose_root }}/{{ stack }}/.env 2>/dev/null; then
        echo "STILL readable by nobody"; exit 1; fi
      echo "mode 600, not readable by nobody"
    changed_when: "false"

  - name: The deploy user can still read it — compose renders as lkraven
    # The assertion that matters. Checking the mode proves the bits changed;
    # only rendering the compose file as the DEPLOY user proves the next
    # deploy can still resolve its variables.
    sudo: true
    shell: |
      su -s /bin/bash -c 'cd {{ compose_root }}/{{ stack }} && docker compose config >/dev/null' {{ expect_owner }} \
        && echo "compose config OK as {{ expect_owner }}" \
        || { echo "COMPOSE CONFIG FAILED as {{ expect_owner }} — reverting is: chmod 644"; exit 1; }
    changed_when: "false"

  - name: Nothing restarted
    sudo: true
    shell: |
      docker ps --filter "name={{ stack }}" --format '{{.Names}} {{.Status}}' | head -3
      echo "(a chmod cannot restart a container; this is a sanity line, not a gate)"
    changed_when: "false"
