Security hardening: policy-based actions, 3-role tokens, and security flags #10

Merged
quentinreytinas merged 11 commits from feature/security-hardening into main 2026-04-30 12:25:03 +02:00
quentinreytinas commented 2026-04-30 11:00:08 +02:00 (Migrated from github.com)

Security Hardening: Policy-Based Automation

Summary

Hardens the Borgmatic API against Node-RED compromise by introducing a policy-based automation layer, role-separated tokens, and granular security flags.

Changes

1. Three-role token system

  • API_ADMIN_TOKEN — Full access (admin, config, locks, passphrase)
  • API_ACTION_TOKEN — Predefined actions only (for Node-RED)
  • API_READ_TOKEN — Read-only (status, health, polling, logs)
  • SECURE_MODE=true enforces all 3 tokens; false preserves legacy API_TOKEN behavior

2. Policy-based actions

  • Actions defined in a YAML policy file (ACTIONS_POLICY_PATH)
  • Strict validation: names, types, SSH URLs, timeouts
  • Node-RED calls POST /actions/<name>/run — no payload, no sensitive params
  • Credentials masked in API responses (target_display)

New endpoints:

Endpoint Method Role Description
/actions GET ACTION+ List actions (safe fields)
/actions/<name>/run POST ACTION+ Trigger action (async)

3. Security flags (5 ENABLE_* env vars)

Flag Controls
ENABLE_ADMIN_ENDPOINTS /emergency/shutdown
ENABLE_CONFIG_WRITE /configs/* PUT/DELETE, /nextcloud/backup-target POST
ENABLE_BREAK_LOCK /borgmatic-locks/break/*
ENABLE_PASSPHRASE_CHANGE /repositories/*/passphrase/change
ENABLE_ARBITRARY_TARGETS /nextcloud/daily-backup/run-for-target*

4. Structured audit logging

  • JSON audit entries: action_start / action_complete
  • Fields: action_name, source_ip, token_role, job_id, target_repo, result, exit_code, duration_sec
  • Configurable: file path (AUDIT_LOG_PATH) and/or stdout (AUDIT_STDOUT)

5. Unit tests (38 tests — all passing)

  • Config validation (SECURE_MODE, legacy mode)
  • Auth (tokens, roles, invalid headers)
  • Actions (policy validation, names, types, SSH URLs, credential masking)
  • Audit (stdout, file writing)

Attack surface if Node-RED is compromised

With API_ACTION_TOKEN Possible
Trigger predefined action Yes
Change backup target No
Break Borg lock No
Change passphrase No
Modify config No
Emergency shutdown No

Files changed

File Status
borgmatic_api_app/config.py Modified
borgmatic_api_app/auth.py Modified
borgmatic_api_app/actions.py Created
borgmatic_api_app/audit.py Created
borgmatic_api_app/routes/actions.py Created
borgmatic_api_app/routes/legacy.py Modified
borgmatic_api_app/services.py Modified
borgmatic_api_app/app.py Modified
tests/test_security.py Created
.env.example Modified
SECURITY_REVIEW.md Created
actions-policy.example.yaml Created

Validation

  • All changes backward-compatible (legacy mode default)
  • 11 files changed, 1349 insertions, 27 deletions
  • Ruff lint: All checks passed
  • Black format: All files formatted
  • pytest: 38 passed in 0.30s

See SECURITY_REVIEW.md for the complete audit document.

# Security Hardening: Policy-Based Automation ## Summary Hardens the Borgmatic API against Node-RED compromise by introducing a policy-based automation layer, role-separated tokens, and granular security flags. ## Changes ### 1. Three-role token system - `API_ADMIN_TOKEN` — Full access (admin, config, locks, passphrase) - `API_ACTION_TOKEN` — Predefined actions only (for Node-RED) - `API_READ_TOKEN` — Read-only (status, health, polling, logs) - `SECURE_MODE=true` enforces all 3 tokens; `false` preserves legacy `API_TOKEN` behavior ### 2. Policy-based actions - Actions defined in a YAML policy file (`ACTIONS_POLICY_PATH`) - Strict validation: names, types, SSH URLs, timeouts - Node-RED calls `POST /actions/<name>/run` — no payload, no sensitive params - Credentials masked in API responses (`target_display`) **New endpoints:** | Endpoint | Method | Role | Description | |----------|--------|------|-------------| | `/actions` | GET | ACTION+ | List actions (safe fields) | | `/actions/<name>/run` | POST | ACTION+ | Trigger action (async) | ### 3. Security flags (5 ENABLE_* env vars) | Flag | Controls | |------|----------| | `ENABLE_ADMIN_ENDPOINTS` | `/emergency/shutdown` | | `ENABLE_CONFIG_WRITE` | `/configs/*` PUT/DELETE, `/nextcloud/backup-target` POST | | `ENABLE_BREAK_LOCK` | `/borgmatic-locks/break/*` | | `ENABLE_PASSPHRASE_CHANGE` | `/repositories/*/passphrase/change` | | `ENABLE_ARBITRARY_TARGETS` | `/nextcloud/daily-backup/run-for-target*` | ### 4. Structured audit logging - JSON audit entries: `action_start` / `action_complete` - Fields: action_name, source_ip, token_role, job_id, target_repo, result, exit_code, duration_sec - Configurable: file path (`AUDIT_LOG_PATH`) and/or stdout (`AUDIT_STDOUT`) ### 5. Unit tests (38 tests — all passing) - Config validation (SECURE_MODE, legacy mode) - Auth (tokens, roles, invalid headers) - Actions (policy validation, names, types, SSH URLs, credential masking) - Audit (stdout, file writing) ## Attack surface if Node-RED is compromised | With API_ACTION_TOKEN | Possible | |-----------------------|----------| | Trigger predefined action | ✅ Yes | | Change backup target | ❌ No | | Break Borg lock | ❌ No | | Change passphrase | ❌ No | | Modify config | ❌ No | | Emergency shutdown | ❌ No | ## Files changed | File | Status | |------|--------| | `borgmatic_api_app/config.py` | Modified | | `borgmatic_api_app/auth.py` | Modified | | `borgmatic_api_app/actions.py` | **Created** | | `borgmatic_api_app/audit.py` | **Created** | | `borgmatic_api_app/routes/actions.py` | **Created** | | `borgmatic_api_app/routes/legacy.py` | Modified | | `borgmatic_api_app/services.py` | Modified | | `borgmatic_api_app/app.py` | Modified | | `tests/test_security.py` | **Created** | | `.env.example` | Modified | | `SECURITY_REVIEW.md` | **Created** | | `actions-policy.example.yaml` | **Created** | ## Validation - All changes backward-compatible (legacy mode default) - 11 files changed, 1349 insertions, 27 deletions - Ruff lint: ✅ All checks passed - Black format: ✅ All files formatted - pytest: ✅ 38 passed in 0.30s See `SECURITY_REVIEW.md` for the complete audit document.
chatgpt-codex-connector[bot] (Migrated from github.com) reviewed 2026-04-30 11:04:14 +02:00
chatgpt-codex-connector[bot] (Migrated from github.com) left a comment

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 34ae54cd76

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

### 💡 Codex Review Here are some automated review suggestions for this pull request. **Reviewed commit:** `34ae54cd76` <details> <summary>ℹ️ About Codex in GitHub</summary> <br/> [Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you - Open a pull request for review - Mark a draft as ready - Comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". </details>
@ -19,12 +23,29 @@ def create_app() -> Flask:
buffers = BufferStore()
chatgpt-codex-connector[bot] (Migrated from github.com) commented 2026-04-30 11:04:14 +02:00

P2 Badge Honor audit stdout/file settings when creating logger

The app factory currently hardcodes stdout=True and drops log_path whenever audit_stdout is enabled, so AUDIT_STDOUT=false is ignored and dual logging (file + stdout) cannot work. This breaks the documented behavior of the audit flags and can prevent required audit persistence depending on deployment settings.

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Honor audit stdout/file settings when creating logger** The app factory currently hardcodes `stdout=True` and drops `log_path` whenever `audit_stdout` is enabled, so `AUDIT_STDOUT=false` is ignored and dual logging (file + stdout) cannot work. This breaks the documented behavior of the audit flags and can prevent required audit persistence depending on deployment settings. Useful? React with 👍 / 👎.
@ -40,12 +61,101 @@ class AuthManager:
if not auth_header.startswith("Bearer "):
chatgpt-codex-connector[bot] (Migrated from github.com) commented 2026-04-30 11:04:14 +02:00

P2 Badge Return 401/403 instead of 500 on role auth failures

require_role() calls auth.require() directly and lets AuthError escape from the decorator. On /actions routes this exception is raised before entering the view-level try/except, so invalid or missing credentials produce a server error response path instead of an auth error response, which is incorrect API behavior for expected unauthenticated requests.

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Return 401/403 instead of 500 on role auth failures** `require_role()` calls `auth.require()` directly and lets `AuthError` escape from the decorator. On `/actions` routes this exception is raised before entering the view-level `try/except`, so invalid or missing credentials produce a server error response path instead of an auth error response, which is incorrect API behavior for expected unauthenticated requests. Useful? React with 👍 / 👎.
@ -0,0 +127,4 @@
def _execute_nextcloud_backup(
services: "Services",
policy: "ActionPolicy", # noqa: F821
job_id: str,
chatgpt-codex-connector[bot] (Migrated from github.com) commented 2026-04-30 11:04:14 +02:00

P1 Badge Call existing backup runner from action worker

The background action worker imports run_nextcloud_daily_backup from borgmatic_api_app.services, but that symbol is not defined there (repo-wide search only finds this call site). As a result, every POST /actions/<name>/run job will hit an ImportError at execution time and fail immediately instead of running the backup.

Useful? React with 👍 / 👎.

**<sub><sub>![P1 Badge](https://img.shields.io/badge/P1-orange?style=flat)</sub></sub> Call existing backup runner from action worker** The background action worker imports `run_nextcloud_daily_backup` from `borgmatic_api_app.services`, but that symbol is not defined there (repo-wide search only finds this call site). As a result, every `POST /actions/<name>/run` job will hit an `ImportError` at execution time and fail immediately instead of running the backup. Useful? React with 👍 / 👎.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
quentinreytinas/borgmatic-api-nextcloud-aio!10
No description provided.