Skip to content

bug(deploy): LimitCORE=infinity is applied regardless of backend_core_capture — stock nodes with working core handling dump process memory containing secrets #13047

Description

@mrveiss

Found reviewing PR #12895 (merged, fa71bc9d2) after the fact. The PR is otherwise sound — the report path is correctly ungated, the sysctl write and the core directory are correctly gated behind backend_core_capture: false. The resource limit that actually permits core writing is not gated at all.

The gap

roles/backend/templates/autobot-backend.service.j2:46:

LimitCORE={{ backend_core_limit | default('infinity') }}

There is no {% if backend_core_capture %} around it, so every node that renders this unit gets LimitCORE=infinity, including the default backend_core_capture: false case.

The inline comment argues this is harmless:

Harmless while capture is off: the kernel still has nowhere to put the core.

That holds only on a node whose core_pattern pipes to a missing handler — which is the specific broken state of the node in #12777, not a general property. On any node with working core handling (systemd-coredump is the distro default on Ubuntu/Debian/Fedora), the unit changes from never writes a core to writes a full process memory image on every abort.

Why that matters

The backend holds decrypted secrets in memory — AUTOBOT_JWT_SECRET, DB credentials, provider API keys, Redis credentials. A core dump is a verbatim image of that memory, written to a world-of-root-readable location, retained by the coredump handler's own policy, and not covered by any of the repo's secret-redaction paths.

Second-order: the backend is a large process and #12777 documents it aborting every ~25-55 minutes. On a node with working capture that is an unbounded disk-fill vector.

Both are latent — they need a node whose core handling works — which is exactly why this will not show up on the #12777 node it was tested against.

Second, smaller defect

backend_core_limit is not declared in roles/backend/defaults/main.yml. The same PR correctly declared backend_core_capture and backend_core_dir there; this one relies solely on the inline Jinja default('infinity'). An undeclared role variable is invisible to anyone reading defaults/, which is where the repo documents every other tunable in this role.

Scope

  • Gate LimitCORE on backend_core_capture, or default it to 0 so the limit follows the opt-in rather than leading it.
  • Declare backend_core_limit in defaults/main.yml with the same recorded-decision comment style used for backend_core_capture.
  • Extend tests/test_backend_service_faulthandler_12777.py: it currently asserts LimitCORE= is present and that backend_core_limit="0" renders LimitCORE=0, but nothing asserts the default-off case — add one pinning the default when backend_core_capture is false.

Done when

  • A stock node with backend_core_capture: false and a working core_pattern writes no core.
  • backend_core_limit appears in defaults/main.yml.
  • A test pins the default-off behaviour.

Refs #12777, #12897, #12895

Activity

  1. github-actions commented on Aug 4, 2026

    @github-actions
    Contributor

    PR #13540 (merged to Dev_new_gui) references this issue with a close keyword.

    security(deploy): gate LimitCORE behind the core-capture opt-in (#13047)

    If this issue is fully resolved, close it manually. If work remains, no action is needed.

  2. mrveiss commented on Aug 4, 2026

    @mrveiss
    OwnerAuthor

    ✅ Delivered — PR #13540, merged to Dev_new_gui.

    Both defects:

    • LimitCORE is now inside {% if backend_core_capture | default(false) | bool %}, matching how core_capture.yml already gates the sysctl write and the core directory. default(false) means an inventory predating the opt-in fails closed.
    • backend_core_limit: infinity declared in defaults/main.yml.

    Rendered three ways:

    capture OFF (the default)  -> (no LimitCORE emitted)
    capture ON  (opt-in)       -> LimitCORE=infinity
    capture var undefined      -> (no LimitCORE emitted)
    

    Environment="PYTHONFAULTHANDLER=1" is deliberately untouched and still unconditional — the two halves of #12777 solve different problems, and only the core dump is a memory-disclosure risk. There is a test asserting it survives, which passes in both directions of the invert-test.

    Two existing tests asserted the defect and were rewritten rather than deleted: test_unit_grants_a_core_limit required LimitCORE= in every render, and test_core_limit_is_overridable rendered an override with capture off. The property they reached for is real, it was just scoped too widely. Their render harness also needed Ansible's bool filter registered, without which the template raises before any assertion runs.

    13 tests in that file now (9 before); both halves invert-tested. Reaches deployed hosts via roles/backend/tasks/unit_only.yml on the next self-update (#12959).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions