diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 00000000..b04555b6 --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,60 @@ +--- +# ansible-lint configuration. +# +# Scoped to rules that catch real breakage. Everything skipped below is +# deliberate and carries a reason, so the skip list stays a short, reviewable +# list rather than a growing pile of suppressions. + +profile: production + +skip_list: + # --- Naming conventions this repo does not follow ------------------------ + # Task variable prefixes (migrations_foo) and task-name casing would mean + # renaming across six duplicated role trees for no functional gain. + - var-naming[no-role-prefix] + - name[casing] + - name[template] + - name[missing-handler] + + # --- Structural choices that are correct as written ---------------------- + # The system/docker-login directory name carries a hyphen. Renaming it + # across every host (and every `- role: system/docker-login` reference) is + # pure churn for a cosmetic rule. + - role-name + + # `collections:` widens the plugin search path for devsec.hardening. Using + # FQCN everywhere instead is arguably tidier, but removing the keyword is a + # behaviour change to every play for no functional benefit. + - fqcn[keyword] + + # `systemctl try-restart` restarts only if the unit is already running, + # whereas systemd_service state=restarted would also start a stopped unit. + # The shell-out is deliberate, so keep it and skip the idiom rule. + - command-instead-of-module + + # Tasks that notify a handler on change are still better as tasks here: the + # crowdsec config edits are conditional on console/bouncer state and read + # more clearly inline than as handlers with matching names. + - no-handler + +exclude_paths: + # Non-Ansible content that happens to live under the tree. + - .github/ + - ci/ + - "*.json" + - "*.lock.hcl" + - "**/files/**" + +warn_list: + - experimental + +# Roles are duplicated per host by design (see README). Linting all six copies +# is intentional: drift between them is the failure mode worth catching. +kinds: + - playbook: "**/ansible/playbook.yml" + - role: "**/ansible/roles/*/" + - tasks: "**/ansible/roles/*/tasks/*.yml" + - handlers: "**/ansible/roles/*/handlers/*.yml" + - defaults: "**/ansible/roles/*/defaults/*.yml" + - vars: "**/ansible/roles/*/vars/*.yml" + - migrations: "**/ansible/migrations/*.yml" diff --git a/.github/actions/setup-lint-env/action.yml b/.github/actions/setup-lint-env/action.yml new file mode 100644 index 00000000..8f74723c --- /dev/null +++ b/.github/actions/setup-lint-env/action.yml @@ -0,0 +1,43 @@ +--- +name: Setup lint environment +description: >- + Set up a uv-managed virtualenv and install the requested packages into it. + + Used by the validation jobs so they share one toolchain with the deploy + workflows rather than each pip-installing into whatever setup-python + provides. uv also caches, which is most of why ansible-lint is no longer the + slow job. + +inputs: + packages: + description: >- + Packages to install, in `uv pip install` syntax. The ansible job passes + `-r requirements.txt` alongside ansible-lint so that ansible-lint uses + the ansible-core the deploys pin, instead of pulling its own copy. + required: true + +runs: + using: composite + steps: + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version-file: .python-version + + - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 + with: + enable-cache: true + cache-dependency-glob: requirements.txt + + - name: Create virtualenv + run: uv venv + shell: bash + + - name: Install packages + run: uv pip install --python .venv/bin/python ${{ inputs.packages }} + shell: bash + + # uv does not put the environment on PATH by itself; without this the + # tools would resolve from whatever setup-python provides. + - name: Put the virtualenv on PATH + run: echo "$PWD/.venv/bin" >> "$GITHUB_PATH" + shell: bash diff --git a/.github/workflows/mirror-ansible-deploy.yml b/.github/workflows/mirror-ansible-deploy.yml index 7911be3e..61333d5d 100644 --- a/.github/workflows/mirror-ansible-deploy.yml +++ b/.github/workflows/mirror-ansible-deploy.yml @@ -1,3 +1,4 @@ +--- name: Deploy mirror with Ansible permissions: contents: read diff --git a/.github/workflows/mirror-tofu-apply.yml b/.github/workflows/mirror-tofu-apply.yml index ec37aad3..898c9d96 100644 --- a/.github/workflows/mirror-tofu-apply.yml +++ b/.github/workflows/mirror-tofu-apply.yml @@ -1,3 +1,4 @@ +--- name: Deploy mirror with OpenTofu permissions: contents: read diff --git a/.github/workflows/mx1-ansible-deploy.yml b/.github/workflows/mx1-ansible-deploy.yml index 30f834b6..3bc96dc4 100644 --- a/.github/workflows/mx1-ansible-deploy.yml +++ b/.github/workflows/mx1-ansible-deploy.yml @@ -1,3 +1,4 @@ +--- name: Deploy mx1 with Ansible permissions: contents: read diff --git a/.github/workflows/tower-ansible-deploy.yml b/.github/workflows/tower-ansible-deploy.yml index 25f12723..99dac937 100644 --- a/.github/workflows/tower-ansible-deploy.yml +++ b/.github/workflows/tower-ansible-deploy.yml @@ -1,3 +1,4 @@ +--- name: Deploy tower with Ansible permissions: contents: read diff --git a/.github/workflows/tower-tofu-apply.yml b/.github/workflows/tower-tofu-apply.yml index 0166154c..28d650ae 100644 --- a/.github/workflows/tower-tofu-apply.yml +++ b/.github/workflows/tower-tofu-apply.yml @@ -1,3 +1,4 @@ +--- name: Deploy tower with OpenTofu permissions: contents: read diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 00000000..def0bf34 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,150 @@ +--- +# Static validation for pull requests. +# +# This workflow runs on `pull_request` and needs no credentials, so it is +# configured to hold none: +# +# 1. It references no `secrets.*` and no `environment:`, so the job runs with +# no access to repository or environment secrets regardless of who opened +# the pull request. zizmor's audits cover the mechanical cases; the rest +# is review. +# 2. `permissions: contents: read` at workflow level — no write scopes and +# no `id-token`. +# 3. Third-party actions are pinned to a full commit SHA, so the code that +# runs with this job's token is fixed. +# 4. Every `actions/checkout` sets `persist-credentials: false`, keeping the +# token out of `.git/config` where later steps could read it. +# +# If a check ever needs a secret, do not add it here. Put it in a separate +# workflow triggered on `push` to a protected branch. + +name: Validate + +on: + pull_request: + branches: + - main + merge_group: + types: + - checks_requested + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: validate-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + yamllint: + name: yamllint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: $/.github/actions/setup-lint-env + with: + packages: yamllint==1.38.0 + + - name: Lint YAML + run: yamllint --strict --config-file .yamllint . + + ansible: + name: ansible-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # requirements.txt first, then ansible-lint, so that ansible-lint runs + # against the ansible-core the deploys pin rather than resolving its own. + # If the two ever conflict, this fails here instead of quietly installing + # a second copy and validating something deploys never run. + - uses: $/.github/actions/setup-lint-env + with: + packages: -r requirements.txt ansible-lint==26.9.0 + + - name: Cache galaxy requirements + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.ansible + key: ansible-galaxy-${{ hashFiles('*/ansible/requirements.yml') }} + restore-keys: ansible-galaxy- + + # `ansible-galaxy install` (not `collection install`) so roles are + # fetched too — requirements.yml lists roles as well as collections, and + # the geerlingguy.docker role is what pulls in community.docker / + # community.general that the tasks actually call. + # + # These are resolved from the PR's requirements.yml, so a PR can point + # that at an arbitrary source. The job holds no credentials, so the + # exposure is limited to a throwaway runner. + - name: Install collections and roles + run: | + for host in mx1 web1 web2 web3 tower mirror; do + if [ -f "$host/ansible/requirements.yml" ]; then + ansible-galaxy install -r "$host/ansible/requirements.yml" --force + fi + done + + - name: Lint playbooks and roles + run: ansible-lint --offline + + tofu: + name: OpenTofu + runs-on: ubuntu-latest + defaults: + run: + working-directory: tower/terraform + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: opentofu/setup-opentofu@a1320f892987e89d278cc92dc5adc984fb93aca4 # v2.0.2 + with: + tofu_version: 1.12.6 + + # -backend=false skips the S3 backend entirely, so validate needs no OCI + # credentials and no state access. + - name: Init without backend + run: tofu init -backend=false -input=false -no-color + + - name: Validate + run: tofu validate -no-color + + - name: Check formatting + run: tofu fmt -check -recursive -diff + + workflows: + name: Workflow lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: $/.github/actions/setup-lint-env + with: + packages: zizmor==1.30.1 + + # Scoped to this workflow on purpose. + # + # This is the only workflow that runs code from a pull request, so it is + # the one where an Actions-level flaw matters most. Auditing it keeps the + # properties in the header comment from regressing. + # + # The deploy workflows are excluded because they legitimately hold + # credentials: they need `secrets.*` to deploy, and two of them chain off + # a provisioning job. Those are sound here because the triggering + # workflow only runs on push-to-main, schedule and manual dispatch. + # zizmor currently reports 42 findings across those eight files, mostly + # `template-injection` on `secrets.*` interpolation and missing + # `persist-credentials: false`. Worth a dedicated hardening pass; not + # this PR. + - name: Audit this workflow + run: zizmor --persona=pedantic --min-severity=low .github/workflows/validate.yml diff --git a/.github/workflows/web1-ansible-deploy.yml b/.github/workflows/web1-ansible-deploy.yml index 43bce68d..5806a963 100644 --- a/.github/workflows/web1-ansible-deploy.yml +++ b/.github/workflows/web1-ansible-deploy.yml @@ -1,3 +1,4 @@ +--- name: Deploy web1 with Ansible permissions: contents: read diff --git a/.github/workflows/web2-ansible-deploy.yml b/.github/workflows/web2-ansible-deploy.yml index 22f82f96..bf66b187 100644 --- a/.github/workflows/web2-ansible-deploy.yml +++ b/.github/workflows/web2-ansible-deploy.yml @@ -1,3 +1,4 @@ +--- name: Deploy web2 with Ansible permissions: contents: read diff --git a/.github/workflows/web3-ansible-deploy.yml b/.github/workflows/web3-ansible-deploy.yml index 36850416..0c85b56b 100644 --- a/.github/workflows/web3-ansible-deploy.yml +++ b/.github/workflows/web3-ansible-deploy.yml @@ -1,3 +1,4 @@ +--- name: Deploy web3 with Ansible permissions: contents: read diff --git a/.yamllint b/.yamllint new file mode 100644 index 00000000..f643f047 --- /dev/null +++ b/.yamllint @@ -0,0 +1,55 @@ +--- +extends: default + +rules: + # The nginx MIME-type replacement strings in system/config are unavoidably + # long. 160 keeps the rule useful for runaway blocks while leaving room for + # digests and URLs (which are non-breakable and allowed regardless). + line-length: + max: 160 + allow-non-breakable-words: true + allow-non-breakable-inline-mappings: false + + # Ansible reads `yes`/`no` as booleans, but `true`/`false` is unambiguous + # across YAML parsers (compose files, GitHub Actions, OpenTofu-adjacent + # tooling). Keys are exempt: task keywords like `when: no` read correctly + # and rewriting them risks behaviour changes. + truthy: + allowed-values: ["true", "false"] + check-keys: false + + # Every YAML file in the repo opens with `---`; keep it enforced so new + # files cannot drift in without the document marker. + document-start: + present: true + + # Ansible task files legitimately mix both sequence styles: role task lists + # sit flush with the parent key (`block:` / `- name:`), while playbook + # `roles:` / `vars_files:` lists are indented. `consistent` accepts either + # as long as a given file does not mix them, which is what actually + # catches real mistakes. + indentation: + spaces: consistent + indent-sequences: consistent + + # Flow mappings in the php_custom_extensions / hardening var files are + # column-aligned for readability. Permit the alignment padding. + braces: + min-spaces-inside: 0 + max-spaces-inside: 1 + commas: + min-spaces-after: 0 + max-spaces-after: 1 + + comments: + min-spaces-from-content: 1 + + # --- Compatibility with `ansible-lint --fix` ----------------------------- + # ansible-lint refuses to autofix when the project .yamllint disagrees with + # its own expectations for the `yaml` rule. Without these it reports "Found + # incompatible custom yamllint configuration" and silently disables fix + # mode, so `fqcn` remediation would never run. + comments-indentation: false + octal-values: + forbid-implicit-octal: true + forbid-explicit-octal: true diff --git a/CLAUDE.md b/CLAUDE.md index 674490f6..a0b63d0f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -117,10 +117,46 @@ Applied to: cloudflared, webmail, n8n runner. - **All data store images** (PostgreSQL, MySQL, Redis, KeyDB, Valkey, OpenSearch, Meilisearch) — their entrypoints start as `root` and use `gosu` to drop to the database user, which requires `CAP_SETUID`/`CAP_SETGID`. Both `cap_drop: [ALL]` and `no-new-privileges:true` break this pattern and prevent the container from starting. - `roundcube` — Apache+PHP image breaks with `cap_drop: [ALL]`; gets `tmpfs: [/tmp]` only. +> **This section is not enforced by CI.** Nothing checks a compose file, so a +> service that omits `cap_drop` passes review only if someone catches it. The +> rules above are the contract; applying them is the reviewer's job. +> +> Two things make this harder to eyeball than it looks. Service names are +> ambiguous across stacks — `worker` is the Docker-socket-holding authentik +> worker in one stack and an ordinary hardened twenty.crm worker in another, so +> "the worker is exempt" is not a safe rule of thumb. And three services +> deliberately publish a port on all interfaces: the MX (it exists to receive +> SMTP), headscale (WireGuard endpoint), frankenphp (terminates ACME +> HTTP-01). Anything else must bind `127.0.0.1`. + ## Adding a Service 1. Create `/containers//docker-compose.yml` 2. Add to `docker_stacks` in `/ansible/roles/system/containers/defaults/main.yml` +3. Apply the container contract above — CI will not check it for you +4. Run `ci/validate.sh` for the checks that do exist + +## Validation + +`ci/validate.sh` runs every check that CI runs: yamllint, ansible-lint, +`tofu validate` / `tofu fmt` for `tower`, and a zizmor audit of the validation +workflow. Run it before pushing; the same failures otherwise surface as a red +PR. + +Configs live in `.yamllint` and `.ansible-lint`. Two things to know when +editing them: + +- `.ansible-lint` `skip_list` entries each carry a reason. Adding a new skip + to silence a finding is a decision that should be argued in a review, not + a reflex — prefer fixing the finding. +- `.yamllint` must keep `comments-indentation: false` and the two + `octal-values` settings. `ansible-lint --fix` silently disables itself when + the project yamllint config disagrees with its expectations, so removing + them breaks `fqcn` remediation without any visible error. + +`ansible-lint --fix fqcn` is safe to run and is how the FQCN migration was +done. Verify with `git diff` afterwards: it should only add module +qualification. ## nftables + Docker + CrowdSec @@ -134,3 +170,13 @@ When nftables is the firewall backend, three things must be correct: net.netfilter.nf_conntrack_tcp_timeout_established: 86400 net.netfilter.nf_conntrack_tcp_timeout_time_wait: 30 ``` + +## Declared dependencies + +`community.docker` and `community.general` are used by the role tree +(`community.docker.docker_compose_v2`, `community.general.ufw`, …) and are +pinned in every host's `ansible/requirements.yml`. They were previously +resolved only as a transitive dependency of `geerlingguy.docker`, which +declares no dependencies of its own — so a clean runner would have failed +`ansible-lint`'s syntax check. Keep them listed. + diff --git a/README.md b/README.md index c91ea624..b19b54bc 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,68 @@ Migrations run automatically as part of the playbook execution via the `system/m If a migration fails, the entire playbook stops to prevent inconsistent states. +## Validation + +Every pull request runs `.github/workflows/validate.yml`, which performs +static checks only. It never connects to a host, reads a state file, or +touches a cloud provider, so a broken change is caught before it can reach +production rather than during a deploy. + +| Check | Tool | Catches | +|---|---|---| +| YAML | `yamllint` | Syntax errors, duplicate keys, style drift | +| Ansible | `ansible-lint` | Broken syntax, missing FQCN, non-idempotent commands, unset file modes | +| OpenTofu | `tofu validate` / `tofu fmt` | Invalid or unformatted configuration for `tower` | +| Actions | `zizmor` | Workflow-level privilege footguns | + +### Running the checks locally + +```bash +ci/validate.sh +``` + +Needs `ansible-lint`, `yamllint`, `zizmor` and `tofu` on `PATH`. The script +runs the same checks as CI, so failures reproduce locally. + +### The container security contract is not enforced + +The rules in `CLAUDE.md` (drop all capabilities, add `no-new-privileges`, bind +ports to loopback, pin image tags) are review-time guidance, not a gate. There +is no automated check, so a new service that omits `cap_drop` will pass CI and +rely on the reviewer noticing. + +This was a deliberate trade-off. An earlier version enforced it with a script +keyed by an exemption list, but the contract is prose in `CLAUDE.md` and would +have had two places to drift apart. It was removed rather than maintained. + +If you want it enforced, the exemption list is the part that needs writing +first — a service name alone is ambiguous, since `worker` is the +Docker-socket-holding authentik worker in one stack and an ordinary hardened +twenty.crm worker in another. + +### Why the validation workflow holds no secrets + +`validate.yml` runs on `pull_request`, so it is configured to hold no +credentials at all — the checks are entirely static analysis: + +- It references no `secrets.*` and no `environment:`, so the job runs with no + access to repository or environment secrets regardless of who opened the + pull request. +- `permissions: contents: read`, so the automatic `GITHUB_TOKEN` cannot write. +- Every `actions/checkout` sets `persist-credentials: false`, so the token is + not left in `.git/config` where later steps could read it. +- Third-party actions are pinned to a full commit SHA, fixing the code that + runs with this job's token. + +All four checks are required status checks on `main`, so a pull request cannot +merge until they pass. If a check is retired or renamed, remove it from the +`required_status_checks.contexts` list in the same pull request — otherwise the +merge blocks waiting on a check that no longer reports. + +The deploy workflows do use secrets — they have to. They are triggered by +`push` to `main`, `schedule` and manual dispatch, never by `pull_request`, so +they never run code from a pull request. + ## Dependency Management This repository uses Renovate to keep dependencies up-to-date: diff --git a/ci/validate.sh b/ci/validate.sh new file mode 100755 index 00000000..5bdbcc6b --- /dev/null +++ b/ci/validate.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Run every check that .github/workflows/validate.yml runs, locally. +# +# CI is the source of truth for what gates a PR; this script exists so the same +# failures show up before pushing rather than after. Keep the two in sync. +# +# Requirements: +# ansible-lint, yamllint, zizmor (pip, or via the repo venv) +# opentofu >= 1.12 (tofu) +# +# Usage: ci/validate.sh [ROOT] + +set -uo pipefail + +ROOT="${1:-.}" +cd "$ROOT" || exit 2 + +status=0 +run() { + local label="$1" + shift + printf '\n=== %s ===\n' "$label" + if "$@"; then + printf 'PASS: %s\n' "$label" + else + printf 'FAIL: %s\n' "$label" + status=1 + fi +} + +have() { command -v "$1" >/dev/null 2>&1; } + +# 1. YAML style and correctness. +if have yamllint; then + run "yamllint" yamllint --strict --config-file .yamllint . +else + printf '\nSKIP: yamllint (not installed)\n' + status=1 +fi + +# 2. Ansible correctness: syntax-check, FQCN, idempotency, risky permissions. +if have ansible-lint; then + run "ansible-lint" ansible-lint --offline +else + printf '\nSKIP: ansible-lint (not installed)\n' + status=1 +fi + +# 3. OpenTofu for the tower host. -backend=false means no OCI credentials and +# no state access are needed, matching CI. +if have tofu; then + run "tofu validate" env -C tower/terraform tofu init -backend=false -input=false -no-color + run "tofu fmt" tofu -chdir=tower/terraform fmt -check -recursive +else + printf '\nSKIP: tofu (not installed)\n' + status=1 +fi + +# 4. Audit this workflow for Actions-level privilege footguns. +if have zizmor; then + run "zizmor" zizmor --persona=pedantic --min-severity=low .github/workflows/validate.yml +else + printf '\nSKIP: zizmor (not installed)\n' + status=1 +fi + +printf '\n' +if [ "$status" -eq 0 ]; then + echo "all checks passed" +else + echo "one or more checks failed" +fi +exit "$status" diff --git a/mirror/ansible/defaults/main.yml b/mirror/ansible/defaults/main.yml index 9861aa0f..efaa817b 100644 --- a/mirror/ansible/defaults/main.yml +++ b/mirror/ansible/defaults/main.yml @@ -4,7 +4,7 @@ dns_search_domain: "{{ lookup('env', 'DNS_SEARCH_DOMAIN') }}" os_env_umask: "022" os_user_pw_ageing: false os_auth_pam_passwdqc_enable: false -ssh_allow_tcp_forwarding: 'local' +ssh_allow_tcp_forwarding: "local" ssh_print_last_log: true ssh_permit_root_login: "without-password" ssh_permit_tunnel: "yes" diff --git a/mirror/ansible/playbook.yml b/mirror/ansible/playbook.yml index a95f7709..558e320c 100644 --- a/mirror/ansible/playbook.yml +++ b/mirror/ansible/playbook.yml @@ -21,10 +21,10 @@ no_log: true block: - name: Apply OS hardening - include_role: + ansible.builtin.include_role: name: devsec.hardening.os_hardening when: scheduled_run - name: Apply ssh hardening - include_role: + ansible.builtin.include_role: name: devsec.hardening.ssh_hardening when: scheduled_run diff --git a/mirror/ansible/requirements.yml b/mirror/ansible/requirements.yml index c542ec9c..6d5eeaf3 100644 --- a/mirror/ansible/requirements.yml +++ b/mirror/ansible/requirements.yml @@ -1,4 +1,9 @@ +--- collections: + - name: community.docker + version: "5.3.0" + - name: community.general + version: "13.4.0" - name: devsec.hardening version: "10.6.0" roles: diff --git a/mirror/ansible/roles/system/apt/tasks/main.yml b/mirror/ansible/roles/system/apt/tasks/main.yml index be2b703d..3962f39a 100644 --- a/mirror/ansible/roles/system/apt/tasks/main.yml +++ b/mirror/ansible/roles/system/apt/tasks/main.yml @@ -1,9 +1,10 @@ +--- - name: Update apt cache, dist-upgrade, and autoremove ansible.builtin.apt: - update_cache: yes + update_cache: true upgrade: dist - autoremove: yes - purge: yes + autoremove: true + purge: true - name: Install base packages ansible.builtin.apt: @@ -12,10 +13,10 @@ state: present - name: Check if reboot required - stat: + ansible.builtin.stat: path: /var/run/reboot-required register: reboot_required_file - name: Reboot if required - reboot: - when: reboot_required_file.stat.exists == true + ansible.builtin.reboot: + when: reboot_required_file.stat.exists diff --git a/mirror/ansible/roles/system/config/handlers/main.yml b/mirror/ansible/roles/system/config/handlers/main.yml index fdd98974..07bfbdac 100644 --- a/mirror/ansible/roles/system/config/handlers/main.yml +++ b/mirror/ansible/roles/system/config/handlers/main.yml @@ -4,6 +4,7 @@ iptables-save > /etc/iptables/rules.v4 ip6tables-save > /etc/iptables/rules.v6 become: true + changed_when: true - name: Restart systemd-resolved ansible.builtin.systemd_service: diff --git a/mirror/ansible/roles/system/config/tasks/main.yml b/mirror/ansible/roles/system/config/tasks/main.yml index 7ca1b77c..0e20acbf 100644 --- a/mirror/ansible/roles/system/config/tasks/main.yml +++ b/mirror/ansible/roles/system/config/tasks/main.yml @@ -7,98 +7,98 @@ - name: Firewall block: - - name: Allow HTTP in iptables - ansible.builtin.iptables: - chain: INPUT - protocol: tcp - destination_port: "80" - ctstate: NEW - jump: ACCEPT - action: insert - become: true - notify: Save iptables rules + - name: Allow HTTP in iptables + ansible.builtin.iptables: + chain: INPUT + protocol: tcp + destination_port: "80" + ctstate: NEW + jump: ACCEPT + action: insert + become: true + notify: Save iptables rules - - name: Allow HTTPS in iptables - ansible.builtin.iptables: - chain: INPUT - protocol: tcp - destination_port: "443" - ctstate: NEW - jump: ACCEPT - action: insert - become: true - notify: Save iptables rules + - name: Allow HTTPS in iptables + ansible.builtin.iptables: + chain: INPUT + protocol: tcp + destination_port: "443" + ctstate: NEW + jump: ACCEPT + action: insert + become: true + notify: Save iptables rules - - name: Allow HTTP in ip6tables - ansible.builtin.iptables: - chain: INPUT - protocol: tcp - destination_port: "80" - ctstate: NEW - jump: ACCEPT - action: insert - ip_version: ipv6 - become: true - notify: Save iptables rules + - name: Allow HTTP in ip6tables + ansible.builtin.iptables: + chain: INPUT + protocol: tcp + destination_port: "80" + ctstate: NEW + jump: ACCEPT + action: insert + ip_version: ipv6 + become: true + notify: Save iptables rules - - name: Allow HTTPS in ip6tables - ansible.builtin.iptables: - chain: INPUT - protocol: tcp - destination_port: "443" - ctstate: NEW - jump: ACCEPT - action: insert - ip_version: ipv6 - become: true - notify: Save iptables rules + - name: Allow HTTPS in ip6tables + ansible.builtin.iptables: + chain: INPUT + protocol: tcp + destination_port: "443" + ctstate: NEW + jump: ACCEPT + action: insert + ip_version: ipv6 + become: true + notify: Save iptables rules - name: DNS block: - - name: Ensure systemd-resolved is running - ansible.builtin.systemd_service: - name: systemd-resolved - state: started - enabled: yes - become: true + - name: Ensure systemd-resolved is running + ansible.builtin.systemd_service: + name: systemd-resolved + state: started + enabled: true + become: true - - name: Configure systemd-resolved for Google and Quad9 DNS - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?DNS=' - line: 'DNS=8.8.8.8 8.8.4.4 9.9.9.9 149.112.112.112' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for Google and Quad9 DNS + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?DNS=" + line: "DNS=8.8.8.8 8.8.4.4 9.9.9.9 149.112.112.112" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for ControlD and Cloudflare Fallback DNS - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?FallbackDNS=' - line: 'FallbackDNS=76.76.2.0 76.76.10.0 1.1.1.1 1.0.0.1' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for ControlD and Cloudflare Fallback DNS + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?FallbackDNS=" + line: "FallbackDNS=76.76.2.0 76.76.10.0 1.1.1.1 1.0.0.1" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for DNSOverTLS - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?DNSOverTLS=' - line: 'DNSOverTLS=no' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for DNSOverTLS + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?DNSOverTLS=" + line: "DNSOverTLS=no" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for Cache - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?Cache=' - line: 'Cache=yes' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for Cache + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?Cache=" + line: "Cache=yes" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for Domain - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?Domain=' - line: "Domain={{ dns_search_domain }}" - notify: Restart systemd-resolved - become: true - when: dns_search_domain | default('') | length > 0 + - name: Configure systemd-resolved for Domain + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?Domain=" + line: "Domain={{ dns_search_domain }}" + notify: Restart systemd-resolved + become: true + when: dns_search_domain | default('') | length > 0 diff --git a/mirror/ansible/roles/system/containers/tasks/deploy_stack.yml b/mirror/ansible/roles/system/containers/tasks/deploy_stack.yml index 090182c2..494046f2 100644 --- a/mirror/ansible/roles/system/containers/tasks/deploy_stack.yml +++ b/mirror/ansible/roles/system/containers/tasks/deploy_stack.yml @@ -9,4 +9,4 @@ project_src: "/opt/containers/{{ item.key }}" state: "{{ item.value.state }}" when: not item.value.env_file or (item.value.env_file and env_stat.stat.exists) - register: compose_result \ No newline at end of file + register: compose_result diff --git a/mirror/ansible/roles/system/containers/tasks/main.yml b/mirror/ansible/roles/system/containers/tasks/main.yml index edc5c1e6..3af63b38 100644 --- a/mirror/ansible/roles/system/containers/tasks/main.yml +++ b/mirror/ansible/roles/system/containers/tasks/main.yml @@ -1,19 +1,19 @@ --- - name: Ensure /opt/containers exists - file: + ansible.builtin.file: path: /opt/containers state: directory owner: root group: root - mode: '0755' + mode: "0755" - name: Copy containers directory to remote node - copy: + ansible.builtin.copy: src: "{{ playbook_dir }}/../containers/" dest: /opt/containers/ owner: root group: root - mode: '0755' + mode: "0755" notify: Restart containers - name: Create shared app-infra Docker network @@ -23,24 +23,24 @@ state: present - name: Create cloudflared .env file - copy: + ansible.builtin.copy: content: | TUNNEL_TOKEN={{ lookup('env', 'CLOUDFLARED_TOKEN') }} dest: /opt/containers/cloudflared/.env owner: root group: root - mode: '0600' + mode: "0600" when: lookup('env', 'CLOUDFLARED_TOKEN') | length > 0 - name: Deploy docker-compose projects - include_tasks: deploy_stack.yml + ansible.builtin.include_tasks: deploy_stack.yml loop: "{{ docker_stacks | dict2items }}" - name: Clean up Docker system community.docker.docker_prune: - containers: yes - images: yes + containers: true + images: true images_filters: dangling: false - networks: yes - builder_cache: yes + networks: true + builder_cache: true diff --git a/mirror/ansible/roles/system/migrations/defaults/main.yml b/mirror/ansible/roles/system/migrations/defaults/main.yml index 25427a89..94346280 100644 --- a/mirror/ansible/roles/system/migrations/defaults/main.yml +++ b/mirror/ansible/roles/system/migrations/defaults/main.yml @@ -1,3 +1,3 @@ --- migration_dir: "{{ playbook_dir }}/migrations" -migration_state_file: /opt/ansible/migrations.db \ No newline at end of file +migration_state_file: /opt/ansible/migrations.db diff --git a/mirror/ansible/roles/system/migrations/tasks/main.yml b/mirror/ansible/roles/system/migrations/tasks/main.yml index 5d801ce6..addef147 100644 --- a/mirror/ansible/roles/system/migrations/tasks/main.yml +++ b/mirror/ansible/roles/system/migrations/tasks/main.yml @@ -1,61 +1,63 @@ --- - name: Ensure sqlite3 is installed - package: + ansible.builtin.package: name: sqlite3 state: present - name: Ensure /opt/ansible directory exists - file: + ansible.builtin.file: path: /opt/ansible state: directory - mode: '0755' + mode: "0755" - name: Ensure migration state database exists - command: sqlite3 {{ migration_state_file }} "CREATE TABLE IF NOT EXISTS migrations (id TEXT PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP);" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "CREATE TABLE IF NOT EXISTS migrations (id TEXT PRIMARY KEY, applied_at DATETIME DEFAULT + CURRENT_TIMESTAMP);" args: creates: "{{ migration_state_file }}" - name: Get applied migrations - command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" register: applied_migrations_output changed_when: false - name: Parse applied migrations - set_fact: + ansible.builtin.set_fact: applied_migrations: "{{ applied_migrations_output.stdout_lines | select | list }}" - name: Show applied migrations - debug: + ansible.builtin.debug: msg: "Already applied migrations: {{ applied_migrations }}" verbosity: 1 no_log: false - name: Find migration files - find: + ansible.builtin.find: paths: "{{ migration_dir }}" patterns: "*.yml" - recurse: no + recurse: false delegate_to: localhost register: migration_files - name: Extract migration IDs from files - set_fact: + ansible.builtin.set_fact: pending_migrations: | {{ migration_files.files | map(attribute='path') | map('basename') | map('regex_replace', '\.yml$', '') | reject('in', applied_migrations) | list }} - name: Show pending migrations - debug: + ansible.builtin.debug: msg: "Pending migrations to apply: {{ pending_migrations }}" verbosity: 1 no_log: false - name: Run pending migrations - include_tasks: "{{ migration_dir }}/{{ item }}.yml" + ansible.builtin.include_tasks: "{{ migration_dir }}/{{ item }}.yml" loop: "{{ pending_migrations }}" register: migration_results - name: Record applied migrations - command: sqlite3 {{ migration_state_file }} "INSERT INTO migrations (id) VALUES ('{{ item }}');" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "INSERT INTO migrations (id) VALUES ('{{ item }}');" + changed_when: true loop: "{{ pending_migrations }}" loop_control: index_var: migration_index @@ -65,12 +67,12 @@ - not (migration_results.results[migration_index].failed | default(false)) - name: Verify migrations were recorded - command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" register: final_migrations changed_when: false - name: Show all recorded migrations - debug: + ansible.builtin.debug: msg: "All recorded migrations: {{ final_migrations.stdout_lines }}" verbosity: 1 - no_log: false \ No newline at end of file + no_log: false diff --git a/mx1/ansible/defaults/main.yml b/mx1/ansible/defaults/main.yml index 8dbc3ba3..96322253 100644 --- a/mx1/ansible/defaults/main.yml +++ b/mx1/ansible/defaults/main.yml @@ -5,7 +5,7 @@ nginx_dh_size: 4096 os_user_pw_ageing: false os_auth_pam_passwdqc_enable: false sftp_enabled: true -ssh_allow_tcp_forwarding: 'local' +ssh_allow_tcp_forwarding: "local" ssh_print_last_log: true ssh_permit_root_login: "without-password" ssh_permit_tunnel: "yes" diff --git a/mx1/ansible/playbook.yml b/mx1/ansible/playbook.yml index 926581fa..b835ee1d 100644 --- a/mx1/ansible/playbook.yml +++ b/mx1/ansible/playbook.yml @@ -21,14 +21,14 @@ no_log: true block: - name: Apply OS hardening - include_role: + ansible.builtin.include_role: name: devsec.hardening.os_hardening when: scheduled_run - name: Apply ssh hardening - include_role: + ansible.builtin.include_role: name: devsec.hardening.ssh_hardening when: scheduled_run - name: Apply nginx hardening - include_role: + ansible.builtin.include_role: name: devsec.hardening.nginx_hardening when: scheduled_run diff --git a/mx1/ansible/requirements.yml b/mx1/ansible/requirements.yml index 3b3c0bbb..1eaa61b3 100644 --- a/mx1/ansible/requirements.yml +++ b/mx1/ansible/requirements.yml @@ -1,4 +1,9 @@ +--- collections: + - name: community.docker + version: "5.3.0" + - name: community.general + version: "13.4.0" - name: devsec.hardening version: "10.6.0" roles: diff --git a/mx1/ansible/roles/system/apt/tasks/main.yml b/mx1/ansible/roles/system/apt/tasks/main.yml index 2268de27..48e45c80 100644 --- a/mx1/ansible/roles/system/apt/tasks/main.yml +++ b/mx1/ansible/roles/system/apt/tasks/main.yml @@ -1,9 +1,10 @@ +--- - name: Update apt cache, dist-upgrade, and autoremove ansible.builtin.apt: - update_cache: yes + update_cache: true upgrade: dist - autoremove: yes - purge: yes + autoremove: true + purge: true - name: Install packages ansible.builtin.apt: @@ -14,28 +15,28 @@ state: present - name: Enable and start qemu-guest-agent - service: + ansible.builtin.service: name: qemu-guest-agent - enabled: yes + enabled: true state: started - name: Enable fstrim timer - service: + ansible.builtin.service: name: fstrim.timer - enabled: yes + enabled: true state: started - name: Ensure nginx is enabled and started - service: + ansible.builtin.service: name: nginx - enabled: yes + enabled: true state: started - name: Check if reboot required - stat: + ansible.builtin.stat: path: /var/run/reboot-required register: reboot_required_file - name: Reboot if required - reboot: - when: reboot_required_file.stat.exists == true + ansible.builtin.reboot: + when: reboot_required_file.stat.exists diff --git a/mx1/ansible/roles/system/config/handlers/main.yml b/mx1/ansible/roles/system/config/handlers/main.yml index 2f3e451c..2f03e43d 100644 --- a/mx1/ansible/roles/system/config/handlers/main.yml +++ b/mx1/ansible/roles/system/config/handlers/main.yml @@ -9,11 +9,11 @@ ansible.builtin.systemd_service: name: logrotate.timer state: restarted - daemon_reload: yes + daemon_reload: true become: true - name: Restart Nginx - service: + ansible.builtin.service: name: nginx state: restarted become: true diff --git a/mx1/ansible/roles/system/config/tasks/main.yml b/mx1/ansible/roles/system/config/tasks/main.yml index cca16585..6166a7a4 100644 --- a/mx1/ansible/roles/system/config/tasks/main.yml +++ b/mx1/ansible/roles/system/config/tasks/main.yml @@ -1,101 +1,101 @@ --- - name: DNS block: - - name: Ensure systemd-resolved is running - ansible.builtin.systemd_service: - name: systemd-resolved - state: started - enabled: yes - become: true + - name: Ensure systemd-resolved is running + ansible.builtin.systemd_service: + name: systemd-resolved + state: started + enabled: true + become: true - - name: Configure systemd-resolved for Google and Quad9 DNS - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?DNS=' - line: 'DNS=8.8.8.8 8.8.4.4 9.9.9.9 149.112.112.112' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for Google and Quad9 DNS + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?DNS=" + line: "DNS=8.8.8.8 8.8.4.4 9.9.9.9 149.112.112.112" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for ControlD and Cloudflare Fallback DNS - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?FallbackDNS=' - line: 'FallbackDNS=76.76.2.0 76.76.10.0 1.1.1.1 1.0.0.1' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for ControlD and Cloudflare Fallback DNS + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?FallbackDNS=" + line: "FallbackDNS=76.76.2.0 76.76.10.0 1.1.1.1 1.0.0.1" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for DNSOverTLS - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?DNSOverTLS=' - line: 'DNSOverTLS=no' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for DNSOverTLS + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?DNSOverTLS=" + line: "DNSOverTLS=no" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for Cache - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?Cache=' - line: 'Cache=yes' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for Cache + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?Cache=" + line: "Cache=yes" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for Domain - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?Domain=' - line: "Domain={{ dns_search_domain }}" - when: dns_search_domain | default('') | length > 0 - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for Domain + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?Domain=" + line: "Domain={{ dns_search_domain }}" + when: dns_search_domain | default('') | length > 0 + notify: Restart systemd-resolved + become: true - name: Nginx Configuration + become: true block: - - name: Ensure Nginx modules are loaded - lineinfile: - path: /etc/nginx/nginx.conf - line: "include /etc/nginx/modules-enabled/*.conf;" - insertbefore: "^events" - notify: Restart Nginx + - name: Ensure Nginx modules are loaded + ansible.builtin.lineinfile: + path: /etc/nginx/nginx.conf + line: "include /etc/nginx/modules-enabled/*.conf;" + insertbefore: "^events" + notify: Restart Nginx - - name: Copy Stalwart stream configuration - template: - src: stalwart.conf.j2 - dest: /etc/nginx/stalwart.conf - owner: root - group: root - mode: '0644' - notify: Restart Nginx + - name: Copy Stalwart stream configuration + ansible.builtin.template: + src: stalwart.conf.j2 + dest: /etc/nginx/stalwart.conf + owner: root + group: root + mode: "0644" + notify: Restart Nginx - - name: Include Stalwart stream config in nginx.conf - lineinfile: - path: /etc/nginx/nginx.conf - line: "include /etc/nginx/stalwart.conf;" - insertafter: EOF - notify: Restart Nginx - become: true + - name: Include Stalwart stream config in nginx.conf + ansible.builtin.lineinfile: + path: /etc/nginx/nginx.conf + line: "include /etc/nginx/stalwart.conf;" + insertafter: EOF + notify: Restart Nginx - name: Logrotate block: - - name: Remove obsolete Docker logrotate configuration - ansible.builtin.file: - path: /etc/logrotate.d/docker - state: absent - become: true + - name: Remove obsolete Docker logrotate configuration + ansible.builtin.file: + path: /etc/logrotate.d/docker + state: absent + become: true - - name: Create logrotate.timer.d directory - ansible.builtin.file: - path: /etc/systemd/system/logrotate.timer.d - state: directory - mode: '0755' - become: true + - name: Create logrotate.timer.d directory + ansible.builtin.file: + path: /etc/systemd/system/logrotate.timer.d + state: directory + mode: "0755" + become: true - - name: Override logrotate.timer - ansible.builtin.copy: - src: files/logrotate.timer.d/override.conf - dest: /etc/systemd/system/logrotate.timer.d/override.conf - owner: root - group: root - mode: '0644' - notify: Restart logrotate.timer - become: true + - name: Override logrotate.timer + ansible.builtin.copy: + src: files/logrotate.timer.d/override.conf + dest: /etc/systemd/system/logrotate.timer.d/override.conf + owner: root + group: root + mode: "0644" + notify: Restart logrotate.timer + become: true diff --git a/mx1/ansible/roles/system/containers/tasks/deploy_stack.yml b/mx1/ansible/roles/system/containers/tasks/deploy_stack.yml index 090182c2..494046f2 100644 --- a/mx1/ansible/roles/system/containers/tasks/deploy_stack.yml +++ b/mx1/ansible/roles/system/containers/tasks/deploy_stack.yml @@ -9,4 +9,4 @@ project_src: "/opt/containers/{{ item.key }}" state: "{{ item.value.state }}" when: not item.value.env_file or (item.value.env_file and env_stat.stat.exists) - register: compose_result \ No newline at end of file + register: compose_result diff --git a/mx1/ansible/roles/system/containers/tasks/main.yml b/mx1/ansible/roles/system/containers/tasks/main.yml index 040f7f51..4ec92320 100644 --- a/mx1/ansible/roles/system/containers/tasks/main.yml +++ b/mx1/ansible/roles/system/containers/tasks/main.yml @@ -1,19 +1,19 @@ --- - name: Ensure /opt/containers exists - file: + ansible.builtin.file: path: /opt/containers state: directory owner: root group: root - mode: '0755' + mode: "0755" - name: Copy containers directory to remote node - copy: + ansible.builtin.copy: src: "{{ playbook_dir }}/../containers/" dest: /opt/containers/ owner: root group: root - mode: '0755' + mode: "0755" notify: Restart containers - name: Create shared app-infra Docker network @@ -26,14 +26,14 @@ state: present - name: Manage stalwart .env entries - lineinfile: + ansible.builtin.lineinfile: path: /opt/containers/stalwart/.env - regexp: '^{{ item.key }}=' + regexp: "^{{ item.key }}=" line: '{{ item.key }}={{ lookup("env", item.value) }}' - create: yes + create: true owner: root group: root - mode: '0600' + mode: "0600" loop: - key: PG_PASS value: STALWART_PG_PASSWORD @@ -42,17 +42,17 @@ when: lookup('env', item.value) | length > 0 - name: Deploy docker-compose projects - include_tasks: deploy_stack.yml + ansible.builtin.include_tasks: deploy_stack.yml loop: "{{ docker_stacks | dict2items }}" - name: Clean up Docker system community.docker.docker_prune: - containers: yes - images: yes + containers: true + images: true images_filters: dangling: false - networks: yes - builder_cache: yes + networks: true + builder_cache: true # - name: Show compose results # debug: diff --git a/mx1/ansible/roles/system/migrations/defaults/main.yml b/mx1/ansible/roles/system/migrations/defaults/main.yml index 25427a89..94346280 100644 --- a/mx1/ansible/roles/system/migrations/defaults/main.yml +++ b/mx1/ansible/roles/system/migrations/defaults/main.yml @@ -1,3 +1,3 @@ --- migration_dir: "{{ playbook_dir }}/migrations" -migration_state_file: /opt/ansible/migrations.db \ No newline at end of file +migration_state_file: /opt/ansible/migrations.db diff --git a/mx1/ansible/roles/system/migrations/tasks/main.yml b/mx1/ansible/roles/system/migrations/tasks/main.yml index 5d801ce6..addef147 100644 --- a/mx1/ansible/roles/system/migrations/tasks/main.yml +++ b/mx1/ansible/roles/system/migrations/tasks/main.yml @@ -1,61 +1,63 @@ --- - name: Ensure sqlite3 is installed - package: + ansible.builtin.package: name: sqlite3 state: present - name: Ensure /opt/ansible directory exists - file: + ansible.builtin.file: path: /opt/ansible state: directory - mode: '0755' + mode: "0755" - name: Ensure migration state database exists - command: sqlite3 {{ migration_state_file }} "CREATE TABLE IF NOT EXISTS migrations (id TEXT PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP);" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "CREATE TABLE IF NOT EXISTS migrations (id TEXT PRIMARY KEY, applied_at DATETIME DEFAULT + CURRENT_TIMESTAMP);" args: creates: "{{ migration_state_file }}" - name: Get applied migrations - command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" register: applied_migrations_output changed_when: false - name: Parse applied migrations - set_fact: + ansible.builtin.set_fact: applied_migrations: "{{ applied_migrations_output.stdout_lines | select | list }}" - name: Show applied migrations - debug: + ansible.builtin.debug: msg: "Already applied migrations: {{ applied_migrations }}" verbosity: 1 no_log: false - name: Find migration files - find: + ansible.builtin.find: paths: "{{ migration_dir }}" patterns: "*.yml" - recurse: no + recurse: false delegate_to: localhost register: migration_files - name: Extract migration IDs from files - set_fact: + ansible.builtin.set_fact: pending_migrations: | {{ migration_files.files | map(attribute='path') | map('basename') | map('regex_replace', '\.yml$', '') | reject('in', applied_migrations) | list }} - name: Show pending migrations - debug: + ansible.builtin.debug: msg: "Pending migrations to apply: {{ pending_migrations }}" verbosity: 1 no_log: false - name: Run pending migrations - include_tasks: "{{ migration_dir }}/{{ item }}.yml" + ansible.builtin.include_tasks: "{{ migration_dir }}/{{ item }}.yml" loop: "{{ pending_migrations }}" register: migration_results - name: Record applied migrations - command: sqlite3 {{ migration_state_file }} "INSERT INTO migrations (id) VALUES ('{{ item }}');" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "INSERT INTO migrations (id) VALUES ('{{ item }}');" + changed_when: true loop: "{{ pending_migrations }}" loop_control: index_var: migration_index @@ -65,12 +67,12 @@ - not (migration_results.results[migration_index].failed | default(false)) - name: Verify migrations were recorded - command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" register: final_migrations changed_when: false - name: Show all recorded migrations - debug: + ansible.builtin.debug: msg: "All recorded migrations: {{ final_migrations.stdout_lines }}" verbosity: 1 - no_log: false \ No newline at end of file + no_log: false diff --git a/mx1/ansible/roles/system/monit/tasks/main.yml b/mx1/ansible/roles/system/monit/tasks/main.yml index e8ec75b1..5b2139c1 100644 --- a/mx1/ansible/roles/system/monit/tasks/main.yml +++ b/mx1/ansible/roles/system/monit/tasks/main.yml @@ -13,7 +13,7 @@ state: directory owner: root group: root - mode: '0755' + mode: "0755" loop: - /etc/monit - /etc/monit.d @@ -25,7 +25,7 @@ dest: /etc/monitrc owner: root group: root - mode: '0600' + mode: "0600" notify: Reload monit become: true @@ -35,7 +35,7 @@ dest: /etc/monit.d/disk.cfg owner: root group: root - mode: '0600' + mode: "0600" notify: Reload monit become: true @@ -45,7 +45,7 @@ dest: /etc/monit/notify-chat.sh owner: root group: root - mode: '0755' + mode: "0755" notify: Reload monit become: true @@ -55,7 +55,7 @@ dest: /etc/monit/check-docker-stack.sh owner: root group: root - mode: '0755' + mode: "0755" notify: Reload monit become: true @@ -65,7 +65,7 @@ dest: /etc/monit.d/containers.cfg owner: root group: root - mode: '0600' + mode: "0600" notify: Reload monit become: true @@ -75,7 +75,7 @@ dest: /etc/monit/notify-chat.env owner: root group: root - mode: '0600' + mode: "0600" force: false become: true @@ -85,7 +85,7 @@ dest: /etc/monit.d/mail.cfg owner: root group: root - mode: '0600' + mode: "0600" force: false become: true diff --git a/tower/ansible/defaults/main.yml b/tower/ansible/defaults/main.yml index 9861aa0f..efaa817b 100644 --- a/tower/ansible/defaults/main.yml +++ b/tower/ansible/defaults/main.yml @@ -4,7 +4,7 @@ dns_search_domain: "{{ lookup('env', 'DNS_SEARCH_DOMAIN') }}" os_env_umask: "022" os_user_pw_ageing: false os_auth_pam_passwdqc_enable: false -ssh_allow_tcp_forwarding: 'local' +ssh_allow_tcp_forwarding: "local" ssh_print_last_log: true ssh_permit_root_login: "without-password" ssh_permit_tunnel: "yes" diff --git a/tower/ansible/playbook.yml b/tower/ansible/playbook.yml index 149b8730..fbaefd95 100644 --- a/tower/ansible/playbook.yml +++ b/tower/ansible/playbook.yml @@ -20,10 +20,10 @@ no_log: true block: - name: Apply OS hardening - include_role: + ansible.builtin.include_role: name: devsec.hardening.os_hardening when: scheduled_run - name: Apply ssh hardening - include_role: + ansible.builtin.include_role: name: devsec.hardening.ssh_hardening when: scheduled_run diff --git a/tower/ansible/requirements.yml b/tower/ansible/requirements.yml index 46066ee4..c7eea3ef 100644 --- a/tower/ansible/requirements.yml +++ b/tower/ansible/requirements.yml @@ -1,4 +1,9 @@ +--- collections: + - name: community.docker + version: "5.3.0" + - name: community.general + version: "13.4.0" - name: devsec.hardening version: "10.6.0" roles: diff --git a/tower/ansible/roles/system/apt/tasks/main.yml b/tower/ansible/roles/system/apt/tasks/main.yml index 544977cb..800f7a56 100644 --- a/tower/ansible/roles/system/apt/tasks/main.yml +++ b/tower/ansible/roles/system/apt/tasks/main.yml @@ -1,15 +1,16 @@ +--- - name: Update apt cache, dist-upgrade, and autoremove ansible.builtin.apt: - update_cache: yes + update_cache: true upgrade: dist - autoremove: yes - purge: yes + autoremove: true + purge: true - name: Check if reboot required - stat: + ansible.builtin.stat: path: /var/run/reboot-required register: reboot_required_file - name: Reboot if required - reboot: - when: reboot_required_file.stat.exists == true + ansible.builtin.reboot: + when: reboot_required_file.stat.exists diff --git a/tower/ansible/roles/system/config/tasks/main.yml b/tower/ansible/roles/system/config/tasks/main.yml index 3c7daa36..1ca5ba41 100644 --- a/tower/ansible/roles/system/config/tasks/main.yml +++ b/tower/ansible/roles/system/config/tasks/main.yml @@ -7,50 +7,50 @@ - name: DNS block: - - name: Ensure systemd-resolved is running - ansible.builtin.systemd_service: - name: systemd-resolved - state: started - enabled: yes - become: true + - name: Ensure systemd-resolved is running + ansible.builtin.systemd_service: + name: systemd-resolved + state: started + enabled: true + become: true - - name: Configure systemd-resolved for Google and Quad9 DNS - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?DNS=' - line: 'DNS=8.8.8.8 8.8.4.4 9.9.9.9 149.112.112.112' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for Google and Quad9 DNS + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?DNS=" + line: "DNS=8.8.8.8 8.8.4.4 9.9.9.9 149.112.112.112" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for ControlD and Cloudflare Fallback DNS - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?FallbackDNS=' - line: 'FallbackDNS=76.76.2.0 76.76.10.0 1.1.1.1 1.0.0.1' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for ControlD and Cloudflare Fallback DNS + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?FallbackDNS=" + line: "FallbackDNS=76.76.2.0 76.76.10.0 1.1.1.1 1.0.0.1" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for DNSOverTLS - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?DNSOverTLS=' - line: 'DNSOverTLS=no' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for DNSOverTLS + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?DNSOverTLS=" + line: "DNSOverTLS=no" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for Cache - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?Cache=' - line: 'Cache=yes' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for Cache + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?Cache=" + line: "Cache=yes" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for Domain - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?Domain=' - line: "Domain={{ dns_search_domain }}" - notify: Restart systemd-resolved - become: true - when: dns_search_domain | default('') | length > 0 + - name: Configure systemd-resolved for Domain + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?Domain=" + line: "Domain={{ dns_search_domain }}" + notify: Restart systemd-resolved + become: true + when: dns_search_domain | default('') | length > 0 diff --git a/tower/ansible/roles/system/containers/tasks/deploy_stack.yml b/tower/ansible/roles/system/containers/tasks/deploy_stack.yml index 090182c2..494046f2 100644 --- a/tower/ansible/roles/system/containers/tasks/deploy_stack.yml +++ b/tower/ansible/roles/system/containers/tasks/deploy_stack.yml @@ -9,4 +9,4 @@ project_src: "/opt/containers/{{ item.key }}" state: "{{ item.value.state }}" when: not item.value.env_file or (item.value.env_file and env_stat.stat.exists) - register: compose_result \ No newline at end of file + register: compose_result diff --git a/tower/ansible/roles/system/containers/tasks/main.yml b/tower/ansible/roles/system/containers/tasks/main.yml index edc5c1e6..3af63b38 100644 --- a/tower/ansible/roles/system/containers/tasks/main.yml +++ b/tower/ansible/roles/system/containers/tasks/main.yml @@ -1,19 +1,19 @@ --- - name: Ensure /opt/containers exists - file: + ansible.builtin.file: path: /opt/containers state: directory owner: root group: root - mode: '0755' + mode: "0755" - name: Copy containers directory to remote node - copy: + ansible.builtin.copy: src: "{{ playbook_dir }}/../containers/" dest: /opt/containers/ owner: root group: root - mode: '0755' + mode: "0755" notify: Restart containers - name: Create shared app-infra Docker network @@ -23,24 +23,24 @@ state: present - name: Create cloudflared .env file - copy: + ansible.builtin.copy: content: | TUNNEL_TOKEN={{ lookup('env', 'CLOUDFLARED_TOKEN') }} dest: /opt/containers/cloudflared/.env owner: root group: root - mode: '0600' + mode: "0600" when: lookup('env', 'CLOUDFLARED_TOKEN') | length > 0 - name: Deploy docker-compose projects - include_tasks: deploy_stack.yml + ansible.builtin.include_tasks: deploy_stack.yml loop: "{{ docker_stacks | dict2items }}" - name: Clean up Docker system community.docker.docker_prune: - containers: yes - images: yes + containers: true + images: true images_filters: dangling: false - networks: yes - builder_cache: yes + networks: true + builder_cache: true diff --git a/tower/ansible/roles/system/migrations/defaults/main.yml b/tower/ansible/roles/system/migrations/defaults/main.yml index 25427a89..94346280 100644 --- a/tower/ansible/roles/system/migrations/defaults/main.yml +++ b/tower/ansible/roles/system/migrations/defaults/main.yml @@ -1,3 +1,3 @@ --- migration_dir: "{{ playbook_dir }}/migrations" -migration_state_file: /opt/ansible/migrations.db \ No newline at end of file +migration_state_file: /opt/ansible/migrations.db diff --git a/tower/ansible/roles/system/migrations/tasks/main.yml b/tower/ansible/roles/system/migrations/tasks/main.yml index 5d801ce6..addef147 100644 --- a/tower/ansible/roles/system/migrations/tasks/main.yml +++ b/tower/ansible/roles/system/migrations/tasks/main.yml @@ -1,61 +1,63 @@ --- - name: Ensure sqlite3 is installed - package: + ansible.builtin.package: name: sqlite3 state: present - name: Ensure /opt/ansible directory exists - file: + ansible.builtin.file: path: /opt/ansible state: directory - mode: '0755' + mode: "0755" - name: Ensure migration state database exists - command: sqlite3 {{ migration_state_file }} "CREATE TABLE IF NOT EXISTS migrations (id TEXT PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP);" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "CREATE TABLE IF NOT EXISTS migrations (id TEXT PRIMARY KEY, applied_at DATETIME DEFAULT + CURRENT_TIMESTAMP);" args: creates: "{{ migration_state_file }}" - name: Get applied migrations - command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" register: applied_migrations_output changed_when: false - name: Parse applied migrations - set_fact: + ansible.builtin.set_fact: applied_migrations: "{{ applied_migrations_output.stdout_lines | select | list }}" - name: Show applied migrations - debug: + ansible.builtin.debug: msg: "Already applied migrations: {{ applied_migrations }}" verbosity: 1 no_log: false - name: Find migration files - find: + ansible.builtin.find: paths: "{{ migration_dir }}" patterns: "*.yml" - recurse: no + recurse: false delegate_to: localhost register: migration_files - name: Extract migration IDs from files - set_fact: + ansible.builtin.set_fact: pending_migrations: | {{ migration_files.files | map(attribute='path') | map('basename') | map('regex_replace', '\.yml$', '') | reject('in', applied_migrations) | list }} - name: Show pending migrations - debug: + ansible.builtin.debug: msg: "Pending migrations to apply: {{ pending_migrations }}" verbosity: 1 no_log: false - name: Run pending migrations - include_tasks: "{{ migration_dir }}/{{ item }}.yml" + ansible.builtin.include_tasks: "{{ migration_dir }}/{{ item }}.yml" loop: "{{ pending_migrations }}" register: migration_results - name: Record applied migrations - command: sqlite3 {{ migration_state_file }} "INSERT INTO migrations (id) VALUES ('{{ item }}');" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "INSERT INTO migrations (id) VALUES ('{{ item }}');" + changed_when: true loop: "{{ pending_migrations }}" loop_control: index_var: migration_index @@ -65,12 +67,12 @@ - not (migration_results.results[migration_index].failed | default(false)) - name: Verify migrations were recorded - command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" register: final_migrations changed_when: false - name: Show all recorded migrations - debug: + ansible.builtin.debug: msg: "All recorded migrations: {{ final_migrations.stdout_lines }}" verbosity: 1 - no_log: false \ No newline at end of file + no_log: false diff --git a/tower/containers/cloudflared/docker-compose.yml b/tower/containers/cloudflared/docker-compose.yml index 82fd5d41..83f75c94 100644 --- a/tower/containers/cloudflared/docker-compose.yml +++ b/tower/containers/cloudflared/docker-compose.yml @@ -1,3 +1,4 @@ +--- services: cloudflared: image: cloudflare/cloudflared:2026.9.3@sha256:072c067d25ccbe61d46e18f0d0723255f2bb5304f7317caa95b27031520ff92c diff --git a/tower/containers/cobalt/docker-compose.yml b/tower/containers/cobalt/docker-compose.yml index d609cfe7..ad58a9fb 100644 --- a/tower/containers/cobalt/docker-compose.yml +++ b/tower/containers/cobalt/docker-compose.yml @@ -1,3 +1,4 @@ +--- services: cobalt-api: image: ghcr.io/imputnet/cobalt:11.7.1 diff --git a/tower/containers/headscale/docker-compose.yml b/tower/containers/headscale/docker-compose.yml index 4a4b4602..a130fac5 100644 --- a/tower/containers/headscale/docker-compose.yml +++ b/tower/containers/headscale/docker-compose.yml @@ -1,3 +1,4 @@ +--- services: headscale: image: headscale/headscale:v0.29.4@sha256:8833f828b414c0907b7e5c71da76473216fe17cce0818a166b536ec552c0903f diff --git a/tower/terraform/.terraform.lock.hcl b/tower/terraform/.terraform.lock.hcl index 598723d6..31d93a0c 100644 --- a/tower/terraform/.terraform.lock.hcl +++ b/tower/terraform/.terraform.lock.hcl @@ -20,6 +20,7 @@ provider "registry.opentofu.org/cloudflare/cloudflare" { "zh:ab0322bcb0d4e465f8603b39f7e743c836b22ae775c03ed677ad999a6a1b8bf4", "zh:c47e38911c92e0b2ffe7be1d36d96b79dd2af79bc61745d6afe5a730cae9f287", "zh:df3f13ae57104ce2f7619cc6c050ab90a5a3299470cdbd2ab8dd3809b84edcb8", + "zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32", "zh:fb7de9d22b02367036c146bd683c8968ec4bbb56aca3185fa84146d118a2bf96", ] } @@ -50,6 +51,7 @@ provider "registry.opentofu.org/oracle/oci" { "zh:74ea3dde5584203fe2566e3da21eeb33ee366c801e36985841acc074c905c992", "zh:774afb82cf093d32ee280cd5d94b26cb49680d5aa28c8a2c49f5c11d9d18ce21", "zh:8c5d47c70d5c9c6242f314ab78cb65682a060f38c77792e5c10b137ee63e1ea6", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", "zh:a474cbf8d4f2742531a51751ba9ae0eb5366ec878355439dc00e0b9c4db8b449", "zh:b0dc7afeca141a2fb3b73c182708543d33aaa0a02ae301de840206e38bbe3eda", "zh:bb6def0e584a85d60d0d395b36793735514f409344fd8539fc756803eb52249a", diff --git a/tower/terraform/network.tf b/tower/terraform/network.tf index 3b7dd47b..85f16632 100644 --- a/tower/terraform/network.tf +++ b/tower/terraform/network.tf @@ -153,9 +153,9 @@ resource "oci_core_security_list" "tower" { } resource "oci_core_subnet" "tower" { - compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.tower.id - cidr_block = var.subnet_cidr + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.tower.id + cidr_block = var.subnet_cidr # Carve a /64 from the VCN's OCI-assigned /56 ipv6cidr_block = cidrsubnet(oci_core_vcn.tower.ipv6cidr_blocks[0], 8, 0) display_name = "${var.instance_display_name}-subnet" diff --git a/web1/ansible/defaults/main.yml b/web1/ansible/defaults/main.yml index 8f38c692..6fdf446d 100644 --- a/web1/ansible/defaults/main.yml +++ b/web1/ansible/defaults/main.yml @@ -6,7 +6,7 @@ os_env_umask: "022" os_user_pw_ageing: false os_auth_pam_passwdqc_enable: false sftp_enabled: true -ssh_allow_tcp_forwarding: 'local' +ssh_allow_tcp_forwarding: "local" ssh_print_last_log: true ssh_permit_root_login: "without-password" ssh_permit_tunnel: "yes" @@ -30,10 +30,10 @@ sysctl_overwrite: php_custom_extensions: "8.4": - { name: brotli, version: "0.18.3", apt_deps: ["libbrotli-dev"] } - - { name: zstd, version: "0.15.2", apt_deps: ["libzstd-dev"] } + - { name: zstd, version: "0.15.2", apt_deps: ["libzstd-dev"] } "8.5": - { name: brotli, version: "0.18.3", apt_deps: ["libbrotli-dev"] } - - { name: zstd, version: "0.15.2", apt_deps: ["libzstd-dev"] } + - { name: zstd, version: "0.15.2", apt_deps: ["libzstd-dev"] } # Bound Docker's default json-file logs for containers that do not override the # driver in their Compose project. diff --git a/web1/ansible/migrations/20260121_0001_relocate_media_for_authentik.yml b/web1/ansible/migrations/20260121_0001_relocate_media_for_authentik.yml index dc0754d2..51d99ff1 100644 --- a/web1/ansible/migrations/20260121_0001_relocate_media_for_authentik.yml +++ b/web1/ansible/migrations/20260121_0001_relocate_media_for_authentik.yml @@ -6,10 +6,10 @@ file: path: /opt/containers/authentik/data state: directory - mode: '0755' + mode: "0755" - name: Relocate authentik media directory command: mv /opt/containers/authentik/media /opt/containers/authentik/data/media args: creates: /opt/containers/authentik/data/media - removes: /opt/containers/authentik/media \ No newline at end of file + removes: /opt/containers/authentik/media diff --git a/web1/ansible/migrations/20260127_0002_switch_crowdsec_bouncer_to_nftables.yml b/web1/ansible/migrations/20260127_0002_switch_crowdsec_bouncer_to_nftables.yml index f6e2e019..4bec56de 100644 --- a/web1/ansible/migrations/20260127_0002_switch_crowdsec_bouncer_to_nftables.yml +++ b/web1/ansible/migrations/20260127_0002_switch_crowdsec_bouncer_to_nftables.yml @@ -15,4 +15,4 @@ - name: Restart CrowdSec firewall bouncer service service: name: crowdsec-firewall-bouncer - state: restarted \ No newline at end of file + state: restarted diff --git a/web1/ansible/migrations/20260204_0003_fix_crowdsec_nftables_priority.yml b/web1/ansible/migrations/20260204_0003_fix_crowdsec_nftables_priority.yml index 4f0ff3a4..e8b4e297 100644 --- a/web1/ansible/migrations/20260204_0003_fix_crowdsec_nftables_priority.yml +++ b/web1/ansible/migrations/20260204_0003_fix_crowdsec_nftables_priority.yml @@ -12,8 +12,8 @@ - name: Update CrowdSec firewall bouncer mode to nftables lineinfile: path: /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml - regexp: '^mode:' - line: 'mode: nftables' + regexp: "^mode:" + line: "mode: nftables" when: bouncer_config.stat.exists - name: Restart CrowdSec firewall bouncer to apply new config diff --git a/web1/ansible/playbook.yml b/web1/ansible/playbook.yml index 3f8fa0ad..2bfd3bf5 100644 --- a/web1/ansible/playbook.yml +++ b/web1/ansible/playbook.yml @@ -24,14 +24,14 @@ no_log: true block: - name: Apply OS hardening - include_role: + ansible.builtin.include_role: name: devsec.hardening.os_hardening when: scheduled_run - name: Apply nginx hardening - include_role: + ansible.builtin.include_role: name: devsec.hardening.nginx_hardening when: scheduled_run - name: Apply ssh hardening - include_role: + ansible.builtin.include_role: name: devsec.hardening.ssh_hardening when: scheduled_run diff --git a/web1/ansible/requirements.yml b/web1/ansible/requirements.yml index 3b3c0bbb..1eaa61b3 100644 --- a/web1/ansible/requirements.yml +++ b/web1/ansible/requirements.yml @@ -1,4 +1,9 @@ +--- collections: + - name: community.docker + version: "5.3.0" + - name: community.general + version: "13.4.0" - name: devsec.hardening version: "10.6.0" roles: diff --git a/web1/ansible/roles/system/apt/tasks/main.yml b/web1/ansible/roles/system/apt/tasks/main.yml index 84c2c0b2..646bfd69 100644 --- a/web1/ansible/roles/system/apt/tasks/main.yml +++ b/web1/ansible/roles/system/apt/tasks/main.yml @@ -1,9 +1,10 @@ +--- - name: Update apt cache, dist-upgrade, and autoremove ansible.builtin.apt: - update_cache: yes + update_cache: true upgrade: dist - autoremove: yes - purge: yes + autoremove: true + purge: true - name: Install packages ansible.builtin.apt: @@ -12,22 +13,22 @@ state: present - name: Enable and start qemu-guest-agent - service: + ansible.builtin.service: name: qemu-guest-agent - enabled: yes + enabled: true state: started - name: Enable fstrim timer - service: + ansible.builtin.service: name: fstrim.timer - enabled: yes + enabled: true state: started - name: Check if reboot required - stat: + ansible.builtin.stat: path: /var/run/reboot-required register: reboot_required_file - name: Reboot if required - reboot: - when: reboot_required_file.stat.exists == true + ansible.builtin.reboot: + when: reboot_required_file.stat.exists diff --git a/web1/ansible/roles/system/backup/tasks/main.yml b/web1/ansible/roles/system/backup/tasks/main.yml index f93bf2a0..df0c4a6b 100644 --- a/web1/ansible/roles/system/backup/tasks/main.yml +++ b/web1/ansible/roles/system/backup/tasks/main.yml @@ -1,6 +1,6 @@ --- - name: Include borgbase.ansible_role_borgbackup role - include_role: + ansible.builtin.include_role: name: borgbase.ansible_role_borgbackup vars: borg_source_directories: @@ -8,7 +8,7 @@ - "/etc" borg_exclude_patterns: - "/home/anatoli" - - "/home/clp" + - "/home/clp" - "/home/mysql" - "*.pyc" - "*.tmp" @@ -39,5 +39,5 @@ name: borgmatic.timer state: started enabled: true - daemon_reload: yes + daemon_reload: true become: true diff --git a/web1/ansible/roles/system/config/handlers/main.yml b/web1/ansible/roles/system/config/handlers/main.yml index f3838ef7..9bb4b1c6 100644 --- a/web1/ansible/roles/system/config/handlers/main.yml +++ b/web1/ansible/roles/system/config/handlers/main.yml @@ -9,7 +9,7 @@ ansible.builtin.systemd_service: name: logrotate.timer state: restarted - daemon_reload: yes + daemon_reload: true become: true - name: Reload nginx diff --git a/web1/ansible/roles/system/config/tasks/main.yml b/web1/ansible/roles/system/config/tasks/main.yml index e71c35c2..3d9755d1 100644 --- a/web1/ansible/roles/system/config/tasks/main.yml +++ b/web1/ansible/roles/system/config/tasks/main.yml @@ -1,89 +1,89 @@ --- - name: UFW block: - - name: Allow incoming UDP port 7844 - community.general.ufw: - rule: allow - proto: udp - from_port: 7844 - comment: Allow incoming traffic from UDP port 7844 - delete: true - become: true + - name: Allow incoming UDP port 7844 + community.general.ufw: + rule: allow + proto: udp + from_port: 7844 + comment: Allow incoming traffic from UDP port 7844 + delete: true + become: true - name: DNS block: - - name: Ensure systemd-resolved is running - ansible.builtin.systemd_service: - name: systemd-resolved - state: started - enabled: yes - become: true + - name: Ensure systemd-resolved is running + ansible.builtin.systemd_service: + name: systemd-resolved + state: started + enabled: true + become: true - - name: Configure systemd-resolved for Google and Quad9 DNS - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?DNS=' - line: 'DNS=8.8.8.8 8.8.4.4 9.9.9.9 149.112.112.112' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for Google and Quad9 DNS + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?DNS=" + line: "DNS=8.8.8.8 8.8.4.4 9.9.9.9 149.112.112.112" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for ControlD and Cloudflare Fallback DNS - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?FallbackDNS=' - line: 'FallbackDNS=76.76.2.0 76.76.10.0 1.1.1.1 1.0.0.1' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for ControlD and Cloudflare Fallback DNS + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?FallbackDNS=" + line: "FallbackDNS=76.76.2.0 76.76.10.0 1.1.1.1 1.0.0.1" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for DNSOverTLS - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?DNSOverTLS=' - line: 'DNSOverTLS=no' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for DNSOverTLS + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?DNSOverTLS=" + line: "DNSOverTLS=no" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for Cache - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?Cache=' - line: 'Cache=yes' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for Cache + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?Cache=" + line: "Cache=yes" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for Domain - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?Domain=' - line: "Domain={{ dns_search_domain }}" - when: dns_search_domain | default('') | length > 0 - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for Domain + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?Domain=" + line: "Domain={{ dns_search_domain }}" + when: dns_search_domain | default('') | length > 0 + notify: Restart systemd-resolved + become: true - name: Logrotate block: - - name: Remove obsolete Docker logrotate configuration - ansible.builtin.file: - path: /etc/logrotate.d/docker - state: absent - become: true + - name: Remove obsolete Docker logrotate configuration + ansible.builtin.file: + path: /etc/logrotate.d/docker + state: absent + become: true - - name: Create logrotate.timer.d directory - ansible.builtin.file: - path: /etc/systemd/system/logrotate.timer.d - state: directory - mode: '0755' - become: true + - name: Create logrotate.timer.d directory + ansible.builtin.file: + path: /etc/systemd/system/logrotate.timer.d + state: directory + mode: "0755" + become: true - - name: Override logrotate.timer - ansible.builtin.copy: - src: files/logrotate.timer.d/override.conf - dest: /etc/systemd/system/logrotate.timer.d/override.conf - owner: root - group: root - mode: '0644' - notify: Restart logrotate.timer - become: true + - name: Override logrotate.timer + ansible.builtin.copy: + src: files/logrotate.timer.d/override.conf + dest: /etc/systemd/system/logrotate.timer.d/override.conf + owner: root + group: root + mode: "0644" + notify: Restart logrotate.timer + become: true - name: Nginx Compression # CloudPanel pre-loads brotli via /etc/nginx/modules-enabled/50-mod-ngx-brotli.conf @@ -95,32 +95,38 @@ - name: Tune gzip_comp_level ansible.builtin.replace: path: /etc/nginx/nginx.conf - regexp: '^(\s*)gzip_comp_level\s+\d+\s*;' - replace: '\g<1>gzip_comp_level 5;' + regexp: "^(\\s*)gzip_comp_level\\s+\\d+\\s*;" + replace: "\\g<1>gzip_comp_level 5;" notify: Reload nginx become: true - name: Tune gzip_types ansible.builtin.replace: path: /etc/nginx/nginx.conf - regexp: '^(\s*)gzip_types[^;]*;' - replace: '\g<1>gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml application/xml+rss application/atom+xml application/rss+xml application/wasm application/manifest+json image/svg+xml font/ttf font/otf application/vnd.ms-fontobject;' + regexp: "^(\\s*)gzip_types[^;]*;" + replace: >- + \g<1>gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml application/xml+rss + application/atom+xml application/rss+xml application/wasm application/manifest+json image/svg+xml font/ttf font/otf + application/vnd.ms-fontobject; notify: Reload nginx become: true - name: Tune brotli_comp_level ansible.builtin.replace: path: /etc/nginx/nginx.conf - regexp: '^(\s*)brotli_comp_level\s+\d+\s*;' - replace: '\g<1>brotli_comp_level 6;' + regexp: "^(\\s*)brotli_comp_level\\s+\\d+\\s*;" + replace: "\\g<1>brotli_comp_level 6;" notify: Reload nginx become: true - name: Tune brotli_types ansible.builtin.replace: path: /etc/nginx/nginx.conf - regexp: '^(\s*)brotli_types[^;]*;' - replace: '\g<1>brotli_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml application/xml+rss application/atom+xml application/rss+xml application/wasm application/manifest+json image/svg+xml font/ttf font/otf application/vnd.ms-fontobject;' + regexp: "^(\\s*)brotli_types[^;]*;" + replace: >- + \g<1>brotli_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml application/xml+rss + application/atom+xml application/rss+xml application/wasm application/manifest+json image/svg+xml font/ttf font/otf + application/vnd.ms-fontobject; notify: Reload nginx become: true diff --git a/web1/ansible/roles/system/containers/tasks/authentik.yml b/web1/ansible/roles/system/containers/tasks/authentik.yml index 616ddbe3..6e72faa9 100644 --- a/web1/ansible/roles/system/containers/tasks/authentik.yml +++ b/web1/ansible/roles/system/containers/tasks/authentik.yml @@ -2,19 +2,19 @@ - name: Setup Authentik Infrastructure block: - name: Create authentik directories - file: + ansible.builtin.file: path: "/opt/containers/authentik/{{ item }}" state: directory owner: "1000" group: "1000" - mode: '0755' + mode: "0755" loop: - media - custom-templates - certs - name: Check if Authentik .env file exists - stat: + ansible.builtin.stat: path: /opt/containers/authentik/.env register: authentik_env_file @@ -27,6 +27,6 @@ when: authentik_env_file.stat.exists - name: Show message when .env file is missing - debug: + ansible.builtin.debug: msg: "Authentik .env file is missing. Please create it manually at /opt/containers/authentik/.env" when: not authentik_env_file.stat.exists diff --git a/web1/ansible/roles/system/containers/tasks/main.yml b/web1/ansible/roles/system/containers/tasks/main.yml index b344d00e..7bc9dfa9 100644 --- a/web1/ansible/roles/system/containers/tasks/main.yml +++ b/web1/ansible/roles/system/containers/tasks/main.yml @@ -1,28 +1,28 @@ --- - name: Ensure /opt/containers exists - file: + ansible.builtin.file: path: /opt/containers state: directory owner: root group: root - mode: '0755' + mode: "0755" - name: Copy OpenCloud Compose project to remote node - copy: + ansible.builtin.copy: src: "{{ playbook_dir }}/../containers/opencloud/" dest: /opt/containers/opencloud/ owner: root group: root - mode: '0755' + mode: "0755" notify: Restart OpenCloud - name: Copy containers directory to remote node - copy: + ansible.builtin.copy: src: "{{ playbook_dir }}/../containers/" dest: /opt/containers/ owner: root group: root - mode: '0755' + mode: "0755" - name: Create shared app-infra Docker network community.docker.docker_network: @@ -37,34 +37,34 @@ state: present - name: Ensure webmail env.file exists - copy: + ansible.builtin.copy: content: "" dest: /opt/containers/webmail/env.file - force: no - mode: '0644' + force: false + mode: "0644" - name: Ensure roundcube .env exists - copy: + ansible.builtin.copy: content: "" dest: /opt/containers/roundcube/.env - force: no - mode: '0644' + force: false + mode: "0644" - name: Setup Authentik Infrastructure - include_tasks: authentik.yml + ansible.builtin.include_tasks: authentik.yml - name: Deploy docker-compose projects - include_tasks: deploy_stack.yml + ansible.builtin.include_tasks: deploy_stack.yml loop: "{{ docker_stacks | dict2items }}" - name: Configure OpenCloud Storage Box and systemd services - include_tasks: opencloud.yml + ansible.builtin.include_tasks: opencloud.yml - name: Clean up Docker system community.docker.docker_prune: - containers: yes - images: yes + containers: true + images: true images_filters: dangling: false - networks: yes - builder_cache: yes + networks: true + builder_cache: true diff --git a/web1/ansible/roles/system/containers/tasks/opencloud.yml b/web1/ansible/roles/system/containers/tasks/opencloud.yml index adf58a49..74494dcc 100644 --- a/web1/ansible/roles/system/containers/tasks/opencloud.yml +++ b/web1/ansible/roles/system/containers/tasks/opencloud.yml @@ -18,11 +18,11 @@ - path: /mnt/opencloud owner: root group: root - mode: '0755' + mode: "0755" - path: /var/lib/opencloud owner: root group: root - mode: '0700' + mode: "0700" - name: Create Storage Box remote placeholder when absent ansible.builtin.copy: @@ -30,7 +30,7 @@ dest: /etc/opencloud-storagebox.env owner: root group: root - mode: '0600' + mode: "0600" force: false - name: Create Storage Box credentials placeholder when absent @@ -39,7 +39,7 @@ dest: /etc/opencloud-storagebox.credentials owner: root group: root - mode: '0600' + mode: "0600" force: false - name: Deploy OpenCloud Storage Box mount helper @@ -48,7 +48,7 @@ dest: /usr/local/sbin/mount-opencloud-storagebox owner: root group: root - mode: '0755' + mode: "0755" notify: Restart OpenCloud - name: Deploy OpenCloud Storage Box compatibility helper @@ -57,7 +57,7 @@ dest: /usr/local/sbin/check-opencloud-storagebox owner: root group: root - mode: '0755' + mode: "0755" notify: Restart OpenCloud - name: Remove obsolete OpenCloud Storage Box condition helper @@ -71,7 +71,7 @@ dest: /etc/systemd/system/opencloud-storagebox.service owner: root group: root - mode: '0644' + mode: "0644" notify: - Reload systemd daemon - Restart OpenCloud @@ -82,7 +82,7 @@ dest: /etc/systemd/system/opencloud.service owner: root group: root - mode: '0644' + mode: "0644" notify: - Reload systemd daemon - Restart OpenCloud @@ -93,7 +93,7 @@ dest: /etc/systemd/system/opencloud-storagebox-compatibility.service owner: root group: root - mode: '0644' + mode: "0644" notify: Reload systemd daemon - name: Reload systemd before OpenCloud service management @@ -172,9 +172,9 @@ ansible.builtin.file: path: /mnt/opencloud/garage state: directory - owner: '1000' - group: '1000' - mode: '0770' + owner: "1000" + group: "1000" + mode: "0770" when: opencloud_host_config_ready - name: Run initial Storage Box persistence probe diff --git a/web1/ansible/roles/system/crowdsec/handlers/main.yml b/web1/ansible/roles/system/crowdsec/handlers/main.yml index 50521d26..fd206978 100644 --- a/web1/ansible/roles/system/crowdsec/handlers/main.yml +++ b/web1/ansible/roles/system/crowdsec/handlers/main.yml @@ -1,9 +1,10 @@ +--- - name: restart crowdsec - service: + ansible.builtin.service: name: crowdsec state: restarted - name: restart crowdsec-firewall-bouncer - service: + ansible.builtin.service: name: crowdsec-firewall-bouncer - state: restarted \ No newline at end of file + state: restarted diff --git a/web1/ansible/roles/system/crowdsec/tasks/main.yml b/web1/ansible/roles/system/crowdsec/tasks/main.yml index 060d0b6b..4110c0d7 100644 --- a/web1/ansible/roles/system/crowdsec/tasks/main.yml +++ b/web1/ansible/roles/system/crowdsec/tasks/main.yml @@ -1,12 +1,13 @@ +--- - name: Read CrowdSec token from environment - set_fact: + ansible.builtin.set_fact: crowdsec_token: "{{ lookup('env', 'CROWDSEC_TOKEN') | default('', true) }}" - name: Download CrowdSec GPG key - get_url: + ansible.builtin.get_url: url: https://packagecloud.io/crowdsec/crowdsec/gpgkey dest: /usr/share/keyrings/crowdsec.asc - mode: '0644' + mode: "0644" - name: Install deb822 repository dependency ansible.builtin.apt: @@ -38,7 +39,7 @@ - name: Update apt cache ansible.builtin.apt: - update_cache: yes + update_cache: true when: crowdsec_repo.changed - name: Install CrowdSec @@ -57,54 +58,56 @@ state: present - name: Deploy CrowdSec configuration - template: + ansible.builtin.template: src: config.yaml.j2 dest: /etc/crowdsec/config.yaml + mode: "0644" notify: restart crowdsec - name: Update local API credentials URL - lineinfile: + ansible.builtin.lineinfile: path: /etc/crowdsec/local_api_credentials.yaml - regexp: '^url:' - line: 'url: http://127.0.0.1:8081' + regexp: "^url:" + line: "url: http://127.0.0.1:8081" - name: Enable and start CrowdSec service - service: + ansible.builtin.service: name: crowdsec - enabled: yes + enabled: true state: started - name: Enable and start CrowdSec firewall bouncer service - service: + ansible.builtin.service: name: crowdsec-firewall-bouncer - enabled: yes + enabled: true state: started - name: Update firewall bouncer API URL - lineinfile: + ansible.builtin.lineinfile: path: /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml - regexp: '^api_url:' - line: 'api_url: http://127.0.0.1:8081/' + regexp: "^api_url:" + line: "api_url: http://127.0.0.1:8081/" notify: restart crowdsec-firewall-bouncer - name: Ensure CrowdSec firewall bouncer uses nftables mode - lineinfile: + ansible.builtin.lineinfile: path: /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml - regexp: '^mode:' - line: 'mode: nftables' + regexp: "^mode:" + line: "mode: nftables" notify: restart crowdsec-firewall-bouncer - name: Update nginx bouncer API URL - lineinfile: + ansible.builtin.lineinfile: path: /etc/crowdsec/bouncers/crowdsec-nginx-bouncer.conf - regexp: '^API_URL=' - line: 'API_URL=http://127.0.0.1:8081' + regexp: "^API_URL=" + line: "API_URL=http://127.0.0.1:8081" - name: Check CrowdSec console enrollment status - command: cscli console status -o json + ansible.builtin.command: cscli console status -o json register: console_status changed_when: false - name: Enroll CrowdSec with console - command: cscli console enroll "{{ crowdsec_token }}" - when: crowdsec_token | length > 0 and 'console_management' not in (console_status.stdout | from_json) \ No newline at end of file + ansible.builtin.command: cscli console enroll "{{ crowdsec_token }}" + changed_when: true + when: crowdsec_token | length > 0 and 'console_management' not in (console_status.stdout | from_json) diff --git a/web1/ansible/roles/system/migrations/defaults/main.yml b/web1/ansible/roles/system/migrations/defaults/main.yml index 25427a89..94346280 100644 --- a/web1/ansible/roles/system/migrations/defaults/main.yml +++ b/web1/ansible/roles/system/migrations/defaults/main.yml @@ -1,3 +1,3 @@ --- migration_dir: "{{ playbook_dir }}/migrations" -migration_state_file: /opt/ansible/migrations.db \ No newline at end of file +migration_state_file: /opt/ansible/migrations.db diff --git a/web1/ansible/roles/system/migrations/tasks/main.yml b/web1/ansible/roles/system/migrations/tasks/main.yml index 5d801ce6..addef147 100644 --- a/web1/ansible/roles/system/migrations/tasks/main.yml +++ b/web1/ansible/roles/system/migrations/tasks/main.yml @@ -1,61 +1,63 @@ --- - name: Ensure sqlite3 is installed - package: + ansible.builtin.package: name: sqlite3 state: present - name: Ensure /opt/ansible directory exists - file: + ansible.builtin.file: path: /opt/ansible state: directory - mode: '0755' + mode: "0755" - name: Ensure migration state database exists - command: sqlite3 {{ migration_state_file }} "CREATE TABLE IF NOT EXISTS migrations (id TEXT PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP);" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "CREATE TABLE IF NOT EXISTS migrations (id TEXT PRIMARY KEY, applied_at DATETIME DEFAULT + CURRENT_TIMESTAMP);" args: creates: "{{ migration_state_file }}" - name: Get applied migrations - command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" register: applied_migrations_output changed_when: false - name: Parse applied migrations - set_fact: + ansible.builtin.set_fact: applied_migrations: "{{ applied_migrations_output.stdout_lines | select | list }}" - name: Show applied migrations - debug: + ansible.builtin.debug: msg: "Already applied migrations: {{ applied_migrations }}" verbosity: 1 no_log: false - name: Find migration files - find: + ansible.builtin.find: paths: "{{ migration_dir }}" patterns: "*.yml" - recurse: no + recurse: false delegate_to: localhost register: migration_files - name: Extract migration IDs from files - set_fact: + ansible.builtin.set_fact: pending_migrations: | {{ migration_files.files | map(attribute='path') | map('basename') | map('regex_replace', '\.yml$', '') | reject('in', applied_migrations) | list }} - name: Show pending migrations - debug: + ansible.builtin.debug: msg: "Pending migrations to apply: {{ pending_migrations }}" verbosity: 1 no_log: false - name: Run pending migrations - include_tasks: "{{ migration_dir }}/{{ item }}.yml" + ansible.builtin.include_tasks: "{{ migration_dir }}/{{ item }}.yml" loop: "{{ pending_migrations }}" register: migration_results - name: Record applied migrations - command: sqlite3 {{ migration_state_file }} "INSERT INTO migrations (id) VALUES ('{{ item }}');" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "INSERT INTO migrations (id) VALUES ('{{ item }}');" + changed_when: true loop: "{{ pending_migrations }}" loop_control: index_var: migration_index @@ -65,12 +67,12 @@ - not (migration_results.results[migration_index].failed | default(false)) - name: Verify migrations were recorded - command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" register: final_migrations changed_when: false - name: Show all recorded migrations - debug: + ansible.builtin.debug: msg: "All recorded migrations: {{ final_migrations.stdout_lines }}" verbosity: 1 - no_log: false \ No newline at end of file + no_log: false diff --git a/web1/ansible/roles/system/monit/tasks/main.yml b/web1/ansible/roles/system/monit/tasks/main.yml index e8ec75b1..5b2139c1 100644 --- a/web1/ansible/roles/system/monit/tasks/main.yml +++ b/web1/ansible/roles/system/monit/tasks/main.yml @@ -13,7 +13,7 @@ state: directory owner: root group: root - mode: '0755' + mode: "0755" loop: - /etc/monit - /etc/monit.d @@ -25,7 +25,7 @@ dest: /etc/monitrc owner: root group: root - mode: '0600' + mode: "0600" notify: Reload monit become: true @@ -35,7 +35,7 @@ dest: /etc/monit.d/disk.cfg owner: root group: root - mode: '0600' + mode: "0600" notify: Reload monit become: true @@ -45,7 +45,7 @@ dest: /etc/monit/notify-chat.sh owner: root group: root - mode: '0755' + mode: "0755" notify: Reload monit become: true @@ -55,7 +55,7 @@ dest: /etc/monit/check-docker-stack.sh owner: root group: root - mode: '0755' + mode: "0755" notify: Reload monit become: true @@ -65,7 +65,7 @@ dest: /etc/monit.d/containers.cfg owner: root group: root - mode: '0600' + mode: "0600" notify: Reload monit become: true @@ -75,7 +75,7 @@ dest: /etc/monit/notify-chat.env owner: root group: root - mode: '0600' + mode: "0600" force: false become: true @@ -85,7 +85,7 @@ dest: /etc/monit.d/mail.cfg owner: root group: root - mode: '0600' + mode: "0600" force: false become: true diff --git a/web1/ansible/roles/system/php/tasks/_build_php_extension.yml b/web1/ansible/roles/system/php/tasks/_build_php_extension.yml index 39226ecd..69c93342 100644 --- a/web1/ansible/roles/system/php/tasks/_build_php_extension.yml +++ b/web1/ansible/roles/system/php/tasks/_build_php_extension.yml @@ -29,13 +29,13 @@ ansible.builtin.file: path: "/tmp/php-ext-build/{{ php_version }}" state: directory - mode: '0755' + mode: "0755" - name: "PHP {{ php_version }} {{ ext.name }} - download PECL tarball" ansible.builtin.get_url: url: "https://pecl.php.net/get/{{ ext.name }}-{{ ext.version }}.tgz" dest: "/tmp/php-ext-build/{{ ext.name }}-{{ ext.version }}.tgz" - mode: '0644' + mode: "0644" - name: "PHP {{ php_version }} {{ ext.name }} - extract source into version-scoped directory" ansible.builtin.unarchive: @@ -54,6 +54,7 @@ args: chdir: "/tmp/php-ext-build/{{ php_version }}/{{ ext.name }}-{{ ext.version }}" executable: /bin/bash + changed_when: true - name: "PHP {{ php_version }} {{ ext.name }} - register in cli php.ini" ansible.builtin.lineinfile: diff --git a/web1/ansible/roles/system/php/tasks/main.yml b/web1/ansible/roles/system/php/tasks/main.yml index edf3390d..4fd6cf58 100644 --- a/web1/ansible/roles/system/php/tasks/main.yml +++ b/web1/ansible/roles/system/php/tasks/main.yml @@ -3,7 +3,7 @@ ansible.builtin.systemd_service: name: "php{{ item }}-fpm" state: stopped - enabled: no + enabled: false loop: - "7.1" - "7.2" @@ -18,7 +18,7 @@ ansible.builtin.file: path: "/etc/php/{{ item }}/fpm/pool.d" mode: "a+r" - recurse: yes + recurse: true loop: - "7.1" - "7.2" diff --git a/web1/containers/1min-relay/docker-compose.yml b/web1/containers/1min-relay/docker-compose.yml index f652da2d..35273444 100644 --- a/web1/containers/1min-relay/docker-compose.yml +++ b/web1/containers/1min-relay/docker-compose.yml @@ -1,3 +1,4 @@ +--- services: 1min-relay: image: ghcr.io/thundersquared/1min-relay:v1.1.0 diff --git a/web1/containers/calcom/docker-compose.yml b/web1/containers/calcom/docker-compose.yml index b354a0a9..d616e6d5 100644 --- a/web1/containers/calcom/docker-compose.yml +++ b/web1/containers/calcom/docker-compose.yml @@ -1,3 +1,4 @@ +--- services: database: image: postgres:18-alpine diff --git a/web1/containers/hermes/docker-compose.yml b/web1/containers/hermes/docker-compose.yml index 744e99de..09d653da 100644 --- a/web1/containers/hermes/docker-compose.yml +++ b/web1/containers/hermes/docker-compose.yml @@ -2,7 +2,7 @@ services: gateway: # renovate: datasource=docker depName=nousresearch/hermes-agent - image: nousresearch/hermes-agent:v2026.9.24@sha256:fca358f12efd65bfaaca05884166f15c0e2788375ca30d77061ac1ebc96452b7 # yamllint disable-line rule:line-length + image: nousresearch/hermes-agent:v2026.9.24@sha256:fca358f12efd65bfaaca05884166f15c0e2788375ca30d77061ac1ebc96452b7 # yamllint disable-line rule:line-length command: ["gateway", "run"] env_file: - .env diff --git a/web1/containers/open-webui/docker-compose.yml b/web1/containers/open-webui/docker-compose.yml index b0f0b40d..2c56144c 100644 --- a/web1/containers/open-webui/docker-compose.yml +++ b/web1/containers/open-webui/docker-compose.yml @@ -1,3 +1,4 @@ +--- services: open-webui: image: ghcr.io/open-webui/open-webui:v0.11.4@sha256:9591b13f13843c7721c2b8eaf7382846c81b3ffe126526d1888d1fed50c6a33f @@ -18,7 +19,6 @@ services: volumes: open_webui: - networks: app-infra: external: true diff --git a/web1/containers/opencloud/csp.yaml b/web1/containers/opencloud/csp.yaml index 4c83ac88..c3c88d43 100644 --- a/web1/containers/opencloud/csp.yaml +++ b/web1/containers/opencloud/csp.yaml @@ -1,53 +1,53 @@ --- directives: child-src: - - '''self''' + - "'self'" connect-src: - - '''self''' - - 'blob:' - - 'https://${COMPANION_DOMAIN|companion.opencloud.test}${TRAEFIK_PORT_HTTPS}/' - - 'wss://${COMPANION_DOMAIN|companion.opencloud.test}${TRAEFIK_PORT_HTTPS}/' - - 'https://raw.githubusercontent.com/opencloud-eu/awesome-apps/' - - 'https://${IDP_DOMAIN|keycloak.opencloud.test}${TRAEFIK_PORT_HTTPS}/' - - 'https://update.opencloud.eu/' - - 'https://tile.openstreetmap.org/' + - "'self'" + - "blob:" + - "https://${COMPANION_DOMAIN|companion.opencloud.test}${TRAEFIK_PORT_HTTPS}/" + - "wss://${COMPANION_DOMAIN|companion.opencloud.test}${TRAEFIK_PORT_HTTPS}/" + - "https://raw.githubusercontent.com/opencloud-eu/awesome-apps/" + - "https://${IDP_DOMAIN|keycloak.opencloud.test}${TRAEFIK_PORT_HTTPS}/" + - "https://update.opencloud.eu/" + - "https://tile.openstreetmap.org/" default-src: - - '''none''' + - "'none'" font-src: - - '''self''' + - "'self'" frame-ancestors: - - '''self''' + - "'self'" frame-src: - - '''self''' - - 'blob:' - - 'https://embed.diagrams.net/' - - 'https://${COLLABORA_DOMAIN|collabora.opencloud.test}${TRAEFIK_PORT_HTTPS}/' - - 'https://${EURO_OFFICE_DOMAIN|euro-office.opencloud.test}${TRAEFIK_PORT_HTTPS}/' - - 'https://docs.opencloud.eu' - - 'https://${IDP_DOMAIN|keycloak.opencloud.test}${TRAEFIK_PORT_HTTPS}/' + - "'self'" + - "blob:" + - "https://embed.diagrams.net/" + - "https://${COLLABORA_DOMAIN|collabora.opencloud.test}${TRAEFIK_PORT_HTTPS}/" + - "https://${EURO_OFFICE_DOMAIN|euro-office.opencloud.test}${TRAEFIK_PORT_HTTPS}/" + - "https://docs.opencloud.eu" + - "https://${IDP_DOMAIN|keycloak.opencloud.test}${TRAEFIK_PORT_HTTPS}/" img-src: - - '''self''' - - 'data:' - - 'blob:' - - 'https://raw.githubusercontent.com/opencloud-eu/awesome-apps/' - - 'https://tile.openstreetmap.org/' - - 'https://${COLLABORA_DOMAIN|collabora.opencloud.test}${TRAEFIK_PORT_HTTPS}/' - - 'https://${EURO_OFFICE_DOMAIN|euro-office.opencloud.test}${TRAEFIK_PORT_HTTPS}/' + - "'self'" + - "data:" + - "blob:" + - "https://raw.githubusercontent.com/opencloud-eu/awesome-apps/" + - "https://tile.openstreetmap.org/" + - "https://${COLLABORA_DOMAIN|collabora.opencloud.test}${TRAEFIK_PORT_HTTPS}/" + - "https://${EURO_OFFICE_DOMAIN|euro-office.opencloud.test}${TRAEFIK_PORT_HTTPS}/" manifest-src: - - '''self''' + - "'self'" media-src: - - '''self''' + - "'self'" object-src: - - '''self''' - - 'blob:' + - "'self'" + - "blob:" script-src: - - '''self''' - - '''unsafe-inline''' - - 'https://${IDP_DOMAIN|keycloak.opencloud.test}${TRAEFIK_PORT_HTTPS}/' + - "'self'" + - "'unsafe-inline'" + - "https://${IDP_DOMAIN|keycloak.opencloud.test}${TRAEFIK_PORT_HTTPS}/" style-src: - - '''self''' - - '''unsafe-inline''' - - 'blob:' + - "'self'" + - "'unsafe-inline'" + - "blob:" worker-src: - "'self'" - - 'blob:' + - "blob:" diff --git a/web1/containers/opencloud/docker-compose.yml b/web1/containers/opencloud/docker-compose.yml index ac602f9d..1479cbe4 100644 --- a/web1/containers/opencloud/docker-compose.yml +++ b/web1/containers/opencloud/docker-compose.yml @@ -54,7 +54,6 @@ volumes: garage_meta: opencloud_config: opencloud_data: - networks: app-infra: external: true diff --git a/web1/containers/roundcube/docker-compose.yml b/web1/containers/roundcube/docker-compose.yml index 5443ffa4..0d39326d 100644 --- a/web1/containers/roundcube/docker-compose.yml +++ b/web1/containers/roundcube/docker-compose.yml @@ -1,3 +1,4 @@ +--- services: roundcubemail: image: roundcube/roundcubemail:1.7.4-apache@sha256:1ccbb2c5909960802b97ce7ec526a50668ad4b139bf8b03817aba64c6b49d58a diff --git a/web1/containers/twenty/docker-compose.yml b/web1/containers/twenty/docker-compose.yml index b39eac7a..e3cba40c 100644 --- a/web1/containers/twenty/docker-compose.yml +++ b/web1/containers/twenty/docker-compose.yml @@ -90,7 +90,6 @@ services: volumes: db: server-local-data: - networks: twenty: app-infra: diff --git a/web1/containers/webmail/docker-compose.yml b/web1/containers/webmail/docker-compose.yml index 9b16d84c..dd4fca89 100644 --- a/web1/containers/webmail/docker-compose.yml +++ b/web1/containers/webmail/docker-compose.yml @@ -1,3 +1,4 @@ +--- services: webmail: image: ghcr.io/linagora/tmail-web:release diff --git a/web2/ansible/defaults/main.yml b/web2/ansible/defaults/main.yml index b792b1f5..8597921d 100644 --- a/web2/ansible/defaults/main.yml +++ b/web2/ansible/defaults/main.yml @@ -4,7 +4,7 @@ dns_search_domain: "{{ lookup('env', 'DNS_SEARCH_DOMAIN') }}" os_user_pw_ageing: false os_auth_pam_passwdqc_enable: false sftp_enabled: true -ssh_allow_tcp_forwarding: 'local' +ssh_allow_tcp_forwarding: "local" ssh_print_last_log: true ssh_permit_root_login: "without-password" ssh_permit_tunnel: "yes" diff --git a/web2/ansible/migrations/20260127_0002_switch_crowdsec_bouncer_to_nftables.yml b/web2/ansible/migrations/20260127_0002_switch_crowdsec_bouncer_to_nftables.yml index f6e2e019..4bec56de 100644 --- a/web2/ansible/migrations/20260127_0002_switch_crowdsec_bouncer_to_nftables.yml +++ b/web2/ansible/migrations/20260127_0002_switch_crowdsec_bouncer_to_nftables.yml @@ -15,4 +15,4 @@ - name: Restart CrowdSec firewall bouncer service service: name: crowdsec-firewall-bouncer - state: restarted \ No newline at end of file + state: restarted diff --git a/web2/ansible/migrations/20260204_0003_fix_crowdsec_nftables_priority.yml b/web2/ansible/migrations/20260204_0003_fix_crowdsec_nftables_priority.yml index 4f0ff3a4..e8b4e297 100644 --- a/web2/ansible/migrations/20260204_0003_fix_crowdsec_nftables_priority.yml +++ b/web2/ansible/migrations/20260204_0003_fix_crowdsec_nftables_priority.yml @@ -12,8 +12,8 @@ - name: Update CrowdSec firewall bouncer mode to nftables lineinfile: path: /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml - regexp: '^mode:' - line: 'mode: nftables' + regexp: "^mode:" + line: "mode: nftables" when: bouncer_config.stat.exists - name: Restart CrowdSec firewall bouncer to apply new config diff --git a/web2/ansible/playbook.yml b/web2/ansible/playbook.yml index b9a54704..25d3bfab 100644 --- a/web2/ansible/playbook.yml +++ b/web2/ansible/playbook.yml @@ -23,10 +23,10 @@ no_log: true block: - name: Apply OS hardening - include_role: + ansible.builtin.include_role: name: devsec.hardening.os_hardening when: scheduled_run - name: Apply ssh hardening - include_role: + ansible.builtin.include_role: name: devsec.hardening.ssh_hardening when: scheduled_run diff --git a/web2/ansible/requirements.yml b/web2/ansible/requirements.yml index 3b3c0bbb..1eaa61b3 100644 --- a/web2/ansible/requirements.yml +++ b/web2/ansible/requirements.yml @@ -1,4 +1,9 @@ +--- collections: + - name: community.docker + version: "5.3.0" + - name: community.general + version: "13.4.0" - name: devsec.hardening version: "10.6.0" roles: diff --git a/web2/ansible/roles/system/apt/tasks/main.yml b/web2/ansible/roles/system/apt/tasks/main.yml index 84c2c0b2..646bfd69 100644 --- a/web2/ansible/roles/system/apt/tasks/main.yml +++ b/web2/ansible/roles/system/apt/tasks/main.yml @@ -1,9 +1,10 @@ +--- - name: Update apt cache, dist-upgrade, and autoremove ansible.builtin.apt: - update_cache: yes + update_cache: true upgrade: dist - autoremove: yes - purge: yes + autoremove: true + purge: true - name: Install packages ansible.builtin.apt: @@ -12,22 +13,22 @@ state: present - name: Enable and start qemu-guest-agent - service: + ansible.builtin.service: name: qemu-guest-agent - enabled: yes + enabled: true state: started - name: Enable fstrim timer - service: + ansible.builtin.service: name: fstrim.timer - enabled: yes + enabled: true state: started - name: Check if reboot required - stat: + ansible.builtin.stat: path: /var/run/reboot-required register: reboot_required_file - name: Reboot if required - reboot: - when: reboot_required_file.stat.exists == true + ansible.builtin.reboot: + when: reboot_required_file.stat.exists diff --git a/web2/ansible/roles/system/backup/tasks/main.yml b/web2/ansible/roles/system/backup/tasks/main.yml index f93bf2a0..df0c4a6b 100644 --- a/web2/ansible/roles/system/backup/tasks/main.yml +++ b/web2/ansible/roles/system/backup/tasks/main.yml @@ -1,6 +1,6 @@ --- - name: Include borgbase.ansible_role_borgbackup role - include_role: + ansible.builtin.include_role: name: borgbase.ansible_role_borgbackup vars: borg_source_directories: @@ -8,7 +8,7 @@ - "/etc" borg_exclude_patterns: - "/home/anatoli" - - "/home/clp" + - "/home/clp" - "/home/mysql" - "*.pyc" - "*.tmp" @@ -39,5 +39,5 @@ name: borgmatic.timer state: started enabled: true - daemon_reload: yes + daemon_reload: true become: true diff --git a/web2/ansible/roles/system/config/handlers/main.yml b/web2/ansible/roles/system/config/handlers/main.yml index 3bbb9178..897869ea 100644 --- a/web2/ansible/roles/system/config/handlers/main.yml +++ b/web2/ansible/roles/system/config/handlers/main.yml @@ -9,5 +9,5 @@ ansible.builtin.systemd_service: name: logrotate.timer state: restarted - daemon_reload: yes + daemon_reload: true become: true diff --git a/web2/ansible/roles/system/config/tasks/main.yml b/web2/ansible/roles/system/config/tasks/main.yml index f65f71d9..85d8bdc9 100644 --- a/web2/ansible/roles/system/config/tasks/main.yml +++ b/web2/ansible/roles/system/config/tasks/main.yml @@ -1,114 +1,114 @@ --- - name: UFW block: - - name: Allow incoming UDP port 7844 - community.general.ufw: - rule: allow - proto: udp - from_port: 7844 - comment: Allow incoming traffic from UDP port 7844 - delete: true - become: true + - name: Allow incoming UDP port 7844 + community.general.ufw: + rule: allow + proto: udp + from_port: 7844 + comment: Allow incoming traffic from UDP port 7844 + delete: true + become: true - # Web traffic for FrankenPHP (HTTP, HTTPS, HTTP/3). Docker publishes these - # ports and inserts its own nftables rules that bypass UFW's INPUT chain, so - # the container is reachable regardless; these rules codify the intended open - # ports and cover any host-level listener. - - name: Allow incoming HTTP (80/tcp) - community.general.ufw: - rule: allow - proto: tcp - port: '80' - comment: HTTP (FrankenPHP / ACME) - become: true + # Web traffic for FrankenPHP (HTTP, HTTPS, HTTP/3). Docker publishes these + # ports and inserts its own nftables rules that bypass UFW's INPUT chain, so + # the container is reachable regardless; these rules codify the intended open + # ports and cover any host-level listener. + - name: Allow incoming HTTP (80/tcp) + community.general.ufw: + rule: allow + proto: tcp + port: "80" + comment: HTTP (FrankenPHP / ACME) + become: true - - name: Allow incoming HTTPS (443/tcp) - community.general.ufw: - rule: allow - proto: tcp - port: '443' - comment: HTTPS (FrankenPHP) - become: true + - name: Allow incoming HTTPS (443/tcp) + community.general.ufw: + rule: allow + proto: tcp + port: "443" + comment: HTTPS (FrankenPHP) + become: true - - name: Allow incoming HTTP/3 (443/udp) - community.general.ufw: - rule: allow - proto: udp - port: '443' - comment: HTTP/3 QUIC (FrankenPHP) - become: true + - name: Allow incoming HTTP/3 (443/udp) + community.general.ufw: + rule: allow + proto: udp + port: "443" + comment: HTTP/3 QUIC (FrankenPHP) + become: true - name: DNS block: - - name: Ensure systemd-resolved is running - ansible.builtin.systemd_service: - name: systemd-resolved - state: started - enabled: yes - become: true + - name: Ensure systemd-resolved is running + ansible.builtin.systemd_service: + name: systemd-resolved + state: started + enabled: true + become: true - - name: Configure systemd-resolved for Google and Quad9 DNS - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?DNS=' - line: 'DNS=8.8.8.8 8.8.4.4 9.9.9.9 149.112.112.112' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for Google and Quad9 DNS + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?DNS=" + line: "DNS=8.8.8.8 8.8.4.4 9.9.9.9 149.112.112.112" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for ControlD and Cloudflare Fallback DNS - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?FallbackDNS=' - line: 'FallbackDNS=76.76.2.0 76.76.10.0 1.1.1.1 1.0.0.1' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for ControlD and Cloudflare Fallback DNS + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?FallbackDNS=" + line: "FallbackDNS=76.76.2.0 76.76.10.0 1.1.1.1 1.0.0.1" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for DNSOverTLS - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?DNSOverTLS=' - line: 'DNSOverTLS=no' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for DNSOverTLS + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?DNSOverTLS=" + line: "DNSOverTLS=no" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for Cache - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?Cache=' - line: 'Cache=yes' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for Cache + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?Cache=" + line: "Cache=yes" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for Domain - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?Domain=' - line: "Domain={{ dns_search_domain }}" - when: dns_search_domain | default('') | length > 0 - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for Domain + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?Domain=" + line: "Domain={{ dns_search_domain }}" + when: dns_search_domain | default('') | length > 0 + notify: Restart systemd-resolved + become: true - name: Logrotate block: - - name: Remove obsolete Docker logrotate configuration - ansible.builtin.file: - path: /etc/logrotate.d/docker - state: absent - become: true + - name: Remove obsolete Docker logrotate configuration + ansible.builtin.file: + path: /etc/logrotate.d/docker + state: absent + become: true - - name: Create logrotate.timer.d directory - ansible.builtin.file: - path: /etc/systemd/system/logrotate.timer.d - state: directory - mode: '0755' - become: true + - name: Create logrotate.timer.d directory + ansible.builtin.file: + path: /etc/systemd/system/logrotate.timer.d + state: directory + mode: "0755" + become: true - - name: Override logrotate.timer - ansible.builtin.copy: - src: files/logrotate.timer.d/override.conf - dest: /etc/systemd/system/logrotate.timer.d/override.conf - owner: root - group: root - mode: '0644' - notify: Restart logrotate.timer - become: true + - name: Override logrotate.timer + ansible.builtin.copy: + src: files/logrotate.timer.d/override.conf + dest: /etc/systemd/system/logrotate.timer.d/override.conf + owner: root + group: root + mode: "0644" + notify: Restart logrotate.timer + become: true diff --git a/web2/ansible/roles/system/containers/tasks/deploy_stack.yml b/web2/ansible/roles/system/containers/tasks/deploy_stack.yml index 090182c2..494046f2 100644 --- a/web2/ansible/roles/system/containers/tasks/deploy_stack.yml +++ b/web2/ansible/roles/system/containers/tasks/deploy_stack.yml @@ -9,4 +9,4 @@ project_src: "/opt/containers/{{ item.key }}" state: "{{ item.value.state }}" when: not item.value.env_file or (item.value.env_file and env_stat.stat.exists) - register: compose_result \ No newline at end of file + register: compose_result diff --git a/web2/ansible/roles/system/containers/tasks/main.yml b/web2/ansible/roles/system/containers/tasks/main.yml index 9e6e5be5..b8472ca2 100644 --- a/web2/ansible/roles/system/containers/tasks/main.yml +++ b/web2/ansible/roles/system/containers/tasks/main.yml @@ -1,19 +1,19 @@ --- - name: Ensure /opt/containers exists - file: + ansible.builtin.file: path: /opt/containers state: directory owner: root group: root - mode: '0755' + mode: "0755" - name: Copy containers directory to remote node - copy: + ansible.builtin.copy: src: "{{ playbook_dir }}/../containers/" dest: /opt/containers/ owner: root group: root - mode: '0755' + mode: "0755" notify: Restart containers - name: Create shared app-infra Docker network @@ -26,13 +26,13 @@ state: present - name: Create cloudflared .env file - copy: + ansible.builtin.copy: content: | TUNNEL_TOKEN={{ lookup('env', 'CLOUDFLARED_TOKEN') }} dest: /opt/containers/cloudflared/.env owner: root group: root - mode: '0600' + mode: "0600" when: lookup('env', 'CLOUDFLARED_TOKEN') | length > 0 # The frankenphp container mounts /opt/hypebun/ read-only as the app @@ -46,7 +46,7 @@ state: directory owner: root group: root - mode: '0755' + mode: "0755" when: "'hypebun-web' in docker_stacks" - name: Find Hypebun environment directories @@ -54,7 +54,7 @@ # they may not exist yet; only normalize the ones present. ansible.builtin.find: paths: /opt/hypebun - patterns: ['prod', 'test'] + patterns: ["prod", "test"] file_type: directory recurse: false register: hypebun_env_dirs @@ -76,14 +76,14 @@ when: "'hypebun-web' in docker_stacks" - name: Deploy docker-compose projects - include_tasks: deploy_stack.yml + ansible.builtin.include_tasks: deploy_stack.yml loop: "{{ docker_stacks | dict2items }}" - name: Clean up Docker system community.docker.docker_prune: - containers: yes - images: yes + containers: true + images: true images_filters: dangling: false - networks: yes - builder_cache: yes + networks: true + builder_cache: true diff --git a/web2/ansible/roles/system/crowdsec/handlers/main.yml b/web2/ansible/roles/system/crowdsec/handlers/main.yml index 50521d26..fd206978 100644 --- a/web2/ansible/roles/system/crowdsec/handlers/main.yml +++ b/web2/ansible/roles/system/crowdsec/handlers/main.yml @@ -1,9 +1,10 @@ +--- - name: restart crowdsec - service: + ansible.builtin.service: name: crowdsec state: restarted - name: restart crowdsec-firewall-bouncer - service: + ansible.builtin.service: name: crowdsec-firewall-bouncer - state: restarted \ No newline at end of file + state: restarted diff --git a/web2/ansible/roles/system/crowdsec/tasks/main.yml b/web2/ansible/roles/system/crowdsec/tasks/main.yml index 8b6b24d6..37f93ba5 100644 --- a/web2/ansible/roles/system/crowdsec/tasks/main.yml +++ b/web2/ansible/roles/system/crowdsec/tasks/main.yml @@ -1,12 +1,13 @@ +--- - name: Read CrowdSec token from environment - set_fact: + ansible.builtin.set_fact: crowdsec_token: "{{ lookup('env', 'CROWDSEC_TOKEN') | default('', true) }}" - name: Download CrowdSec GPG key - get_url: + ansible.builtin.get_url: url: https://packagecloud.io/crowdsec/crowdsec/gpgkey dest: /usr/share/keyrings/crowdsec.asc - mode: '0644' + mode: "0644" - name: Install deb822 repository dependency ansible.builtin.apt: @@ -38,7 +39,7 @@ - name: Update apt cache ansible.builtin.apt: - update_cache: yes + update_cache: true when: crowdsec_repo.changed - name: Install CrowdSec @@ -52,48 +53,50 @@ state: present - name: Deploy CrowdSec configuration - template: + ansible.builtin.template: src: config.yaml.j2 dest: /etc/crowdsec/config.yaml + mode: "0644" notify: restart crowdsec - name: Update local API credentials URL - lineinfile: + ansible.builtin.lineinfile: path: /etc/crowdsec/local_api_credentials.yaml - regexp: '^url:' - line: 'url: http://127.0.0.1:8081' + regexp: "^url:" + line: "url: http://127.0.0.1:8081" - name: Enable and start CrowdSec service - service: + ansible.builtin.service: name: crowdsec - enabled: yes + enabled: true state: started - name: Enable and start CrowdSec firewall bouncer service - service: + ansible.builtin.service: name: crowdsec-firewall-bouncer - enabled: yes + enabled: true state: started - name: Update firewall bouncer API URL - lineinfile: + ansible.builtin.lineinfile: path: /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml - regexp: '^api_url:' - line: 'api_url: http://127.0.0.1:8081/' + regexp: "^api_url:" + line: "api_url: http://127.0.0.1:8081/" notify: restart crowdsec-firewall-bouncer - name: Ensure CrowdSec firewall bouncer uses nftables mode - lineinfile: + ansible.builtin.lineinfile: path: /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml - regexp: '^mode:' - line: 'mode: nftables' + regexp: "^mode:" + line: "mode: nftables" notify: restart crowdsec-firewall-bouncer - name: Check CrowdSec console enrollment status - command: cscli console status -o json + ansible.builtin.command: cscli console status -o json register: console_status changed_when: false - name: Enroll CrowdSec with console - command: cscli console enroll "{{ crowdsec_token }}" - when: crowdsec_token | length > 0 and 'console_management' not in (console_status.stdout | from_json) \ No newline at end of file + ansible.builtin.command: cscli console enroll "{{ crowdsec_token }}" + changed_when: true + when: crowdsec_token | length > 0 and 'console_management' not in (console_status.stdout | from_json) diff --git a/web2/ansible/roles/system/migrations/defaults/main.yml b/web2/ansible/roles/system/migrations/defaults/main.yml index 25427a89..94346280 100644 --- a/web2/ansible/roles/system/migrations/defaults/main.yml +++ b/web2/ansible/roles/system/migrations/defaults/main.yml @@ -1,3 +1,3 @@ --- migration_dir: "{{ playbook_dir }}/migrations" -migration_state_file: /opt/ansible/migrations.db \ No newline at end of file +migration_state_file: /opt/ansible/migrations.db diff --git a/web2/ansible/roles/system/migrations/tasks/main.yml b/web2/ansible/roles/system/migrations/tasks/main.yml index 5d801ce6..addef147 100644 --- a/web2/ansible/roles/system/migrations/tasks/main.yml +++ b/web2/ansible/roles/system/migrations/tasks/main.yml @@ -1,61 +1,63 @@ --- - name: Ensure sqlite3 is installed - package: + ansible.builtin.package: name: sqlite3 state: present - name: Ensure /opt/ansible directory exists - file: + ansible.builtin.file: path: /opt/ansible state: directory - mode: '0755' + mode: "0755" - name: Ensure migration state database exists - command: sqlite3 {{ migration_state_file }} "CREATE TABLE IF NOT EXISTS migrations (id TEXT PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP);" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "CREATE TABLE IF NOT EXISTS migrations (id TEXT PRIMARY KEY, applied_at DATETIME DEFAULT + CURRENT_TIMESTAMP);" args: creates: "{{ migration_state_file }}" - name: Get applied migrations - command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" register: applied_migrations_output changed_when: false - name: Parse applied migrations - set_fact: + ansible.builtin.set_fact: applied_migrations: "{{ applied_migrations_output.stdout_lines | select | list }}" - name: Show applied migrations - debug: + ansible.builtin.debug: msg: "Already applied migrations: {{ applied_migrations }}" verbosity: 1 no_log: false - name: Find migration files - find: + ansible.builtin.find: paths: "{{ migration_dir }}" patterns: "*.yml" - recurse: no + recurse: false delegate_to: localhost register: migration_files - name: Extract migration IDs from files - set_fact: + ansible.builtin.set_fact: pending_migrations: | {{ migration_files.files | map(attribute='path') | map('basename') | map('regex_replace', '\.yml$', '') | reject('in', applied_migrations) | list }} - name: Show pending migrations - debug: + ansible.builtin.debug: msg: "Pending migrations to apply: {{ pending_migrations }}" verbosity: 1 no_log: false - name: Run pending migrations - include_tasks: "{{ migration_dir }}/{{ item }}.yml" + ansible.builtin.include_tasks: "{{ migration_dir }}/{{ item }}.yml" loop: "{{ pending_migrations }}" register: migration_results - name: Record applied migrations - command: sqlite3 {{ migration_state_file }} "INSERT INTO migrations (id) VALUES ('{{ item }}');" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "INSERT INTO migrations (id) VALUES ('{{ item }}');" + changed_when: true loop: "{{ pending_migrations }}" loop_control: index_var: migration_index @@ -65,12 +67,12 @@ - not (migration_results.results[migration_index].failed | default(false)) - name: Verify migrations were recorded - command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" register: final_migrations changed_when: false - name: Show all recorded migrations - debug: + ansible.builtin.debug: msg: "All recorded migrations: {{ final_migrations.stdout_lines }}" verbosity: 1 - no_log: false \ No newline at end of file + no_log: false diff --git a/web2/ansible/roles/system/monit/tasks/main.yml b/web2/ansible/roles/system/monit/tasks/main.yml index e8ec75b1..5b2139c1 100644 --- a/web2/ansible/roles/system/monit/tasks/main.yml +++ b/web2/ansible/roles/system/monit/tasks/main.yml @@ -13,7 +13,7 @@ state: directory owner: root group: root - mode: '0755' + mode: "0755" loop: - /etc/monit - /etc/monit.d @@ -25,7 +25,7 @@ dest: /etc/monitrc owner: root group: root - mode: '0600' + mode: "0600" notify: Reload monit become: true @@ -35,7 +35,7 @@ dest: /etc/monit.d/disk.cfg owner: root group: root - mode: '0600' + mode: "0600" notify: Reload monit become: true @@ -45,7 +45,7 @@ dest: /etc/monit/notify-chat.sh owner: root group: root - mode: '0755' + mode: "0755" notify: Reload monit become: true @@ -55,7 +55,7 @@ dest: /etc/monit/check-docker-stack.sh owner: root group: root - mode: '0755' + mode: "0755" notify: Reload monit become: true @@ -65,7 +65,7 @@ dest: /etc/monit.d/containers.cfg owner: root group: root - mode: '0600' + mode: "0600" notify: Reload monit become: true @@ -75,7 +75,7 @@ dest: /etc/monit/notify-chat.env owner: root group: root - mode: '0600' + mode: "0600" force: false become: true @@ -85,7 +85,7 @@ dest: /etc/monit.d/mail.cfg owner: root group: root - mode: '0600' + mode: "0600" force: false become: true diff --git a/web2/containers/cloudflared/docker-compose.yml b/web2/containers/cloudflared/docker-compose.yml index 82fd5d41..83f75c94 100644 --- a/web2/containers/cloudflared/docker-compose.yml +++ b/web2/containers/cloudflared/docker-compose.yml @@ -1,3 +1,4 @@ +--- services: cloudflared: image: cloudflare/cloudflared:2026.9.3@sha256:072c067d25ccbe61d46e18f0d0723255f2bb5304f7317caa95b27031520ff92c diff --git a/web2/containers/cobalt/docker-compose.yml b/web2/containers/cobalt/docker-compose.yml index d609cfe7..ad58a9fb 100644 --- a/web2/containers/cobalt/docker-compose.yml +++ b/web2/containers/cobalt/docker-compose.yml @@ -1,3 +1,4 @@ +--- services: cobalt-api: image: ghcr.io/imputnet/cobalt:11.7.1 diff --git a/web2/containers/hypebun-web/docker-compose.yml b/web2/containers/hypebun-web/docker-compose.yml index 3b79840f..09082fa4 100644 --- a/web2/containers/hypebun-web/docker-compose.yml +++ b/web2/containers/hypebun-web/docker-compose.yml @@ -1,3 +1,4 @@ +--- services: frankenphp: # Custom image: base FrankenPHP + mysqli/gd/intl/... required by AltumCode. diff --git a/web3/ansible/defaults/main.yml b/web3/ansible/defaults/main.yml index 8f38c692..6fdf446d 100644 --- a/web3/ansible/defaults/main.yml +++ b/web3/ansible/defaults/main.yml @@ -6,7 +6,7 @@ os_env_umask: "022" os_user_pw_ageing: false os_auth_pam_passwdqc_enable: false sftp_enabled: true -ssh_allow_tcp_forwarding: 'local' +ssh_allow_tcp_forwarding: "local" ssh_print_last_log: true ssh_permit_root_login: "without-password" ssh_permit_tunnel: "yes" @@ -30,10 +30,10 @@ sysctl_overwrite: php_custom_extensions: "8.4": - { name: brotli, version: "0.18.3", apt_deps: ["libbrotli-dev"] } - - { name: zstd, version: "0.15.2", apt_deps: ["libzstd-dev"] } + - { name: zstd, version: "0.15.2", apt_deps: ["libzstd-dev"] } "8.5": - { name: brotli, version: "0.18.3", apt_deps: ["libbrotli-dev"] } - - { name: zstd, version: "0.15.2", apt_deps: ["libzstd-dev"] } + - { name: zstd, version: "0.15.2", apt_deps: ["libzstd-dev"] } # Bound Docker's default json-file logs for containers that do not override the # driver in their Compose project. diff --git a/web3/ansible/migrations/20260121_0001_relocate_media_for_authentik.yml b/web3/ansible/migrations/20260121_0001_relocate_media_for_authentik.yml index dc0754d2..51d99ff1 100644 --- a/web3/ansible/migrations/20260121_0001_relocate_media_for_authentik.yml +++ b/web3/ansible/migrations/20260121_0001_relocate_media_for_authentik.yml @@ -6,10 +6,10 @@ file: path: /opt/containers/authentik/data state: directory - mode: '0755' + mode: "0755" - name: Relocate authentik media directory command: mv /opt/containers/authentik/media /opt/containers/authentik/data/media args: creates: /opt/containers/authentik/data/media - removes: /opt/containers/authentik/media \ No newline at end of file + removes: /opt/containers/authentik/media diff --git a/web3/ansible/migrations/20260127_0002_switch_crowdsec_bouncer_to_nftables.yml b/web3/ansible/migrations/20260127_0002_switch_crowdsec_bouncer_to_nftables.yml index f6e2e019..4bec56de 100644 --- a/web3/ansible/migrations/20260127_0002_switch_crowdsec_bouncer_to_nftables.yml +++ b/web3/ansible/migrations/20260127_0002_switch_crowdsec_bouncer_to_nftables.yml @@ -15,4 +15,4 @@ - name: Restart CrowdSec firewall bouncer service service: name: crowdsec-firewall-bouncer - state: restarted \ No newline at end of file + state: restarted diff --git a/web3/ansible/migrations/20260204_0003_fix_crowdsec_nftables_priority.yml b/web3/ansible/migrations/20260204_0003_fix_crowdsec_nftables_priority.yml index 4f0ff3a4..e8b4e297 100644 --- a/web3/ansible/migrations/20260204_0003_fix_crowdsec_nftables_priority.yml +++ b/web3/ansible/migrations/20260204_0003_fix_crowdsec_nftables_priority.yml @@ -12,8 +12,8 @@ - name: Update CrowdSec firewall bouncer mode to nftables lineinfile: path: /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml - regexp: '^mode:' - line: 'mode: nftables' + regexp: "^mode:" + line: "mode: nftables" when: bouncer_config.stat.exists - name: Restart CrowdSec firewall bouncer to apply new config diff --git a/web3/ansible/playbook.yml b/web3/ansible/playbook.yml index 3f8fa0ad..2bfd3bf5 100644 --- a/web3/ansible/playbook.yml +++ b/web3/ansible/playbook.yml @@ -24,14 +24,14 @@ no_log: true block: - name: Apply OS hardening - include_role: + ansible.builtin.include_role: name: devsec.hardening.os_hardening when: scheduled_run - name: Apply nginx hardening - include_role: + ansible.builtin.include_role: name: devsec.hardening.nginx_hardening when: scheduled_run - name: Apply ssh hardening - include_role: + ansible.builtin.include_role: name: devsec.hardening.ssh_hardening when: scheduled_run diff --git a/web3/ansible/requirements.yml b/web3/ansible/requirements.yml index 3b3c0bbb..1eaa61b3 100644 --- a/web3/ansible/requirements.yml +++ b/web3/ansible/requirements.yml @@ -1,4 +1,9 @@ +--- collections: + - name: community.docker + version: "5.3.0" + - name: community.general + version: "13.4.0" - name: devsec.hardening version: "10.6.0" roles: diff --git a/web3/ansible/roles/system/apt/tasks/main.yml b/web3/ansible/roles/system/apt/tasks/main.yml index 84c2c0b2..646bfd69 100644 --- a/web3/ansible/roles/system/apt/tasks/main.yml +++ b/web3/ansible/roles/system/apt/tasks/main.yml @@ -1,9 +1,10 @@ +--- - name: Update apt cache, dist-upgrade, and autoremove ansible.builtin.apt: - update_cache: yes + update_cache: true upgrade: dist - autoremove: yes - purge: yes + autoremove: true + purge: true - name: Install packages ansible.builtin.apt: @@ -12,22 +13,22 @@ state: present - name: Enable and start qemu-guest-agent - service: + ansible.builtin.service: name: qemu-guest-agent - enabled: yes + enabled: true state: started - name: Enable fstrim timer - service: + ansible.builtin.service: name: fstrim.timer - enabled: yes + enabled: true state: started - name: Check if reboot required - stat: + ansible.builtin.stat: path: /var/run/reboot-required register: reboot_required_file - name: Reboot if required - reboot: - when: reboot_required_file.stat.exists == true + ansible.builtin.reboot: + when: reboot_required_file.stat.exists diff --git a/web3/ansible/roles/system/backup/tasks/main.yml b/web3/ansible/roles/system/backup/tasks/main.yml index f93bf2a0..df0c4a6b 100644 --- a/web3/ansible/roles/system/backup/tasks/main.yml +++ b/web3/ansible/roles/system/backup/tasks/main.yml @@ -1,6 +1,6 @@ --- - name: Include borgbase.ansible_role_borgbackup role - include_role: + ansible.builtin.include_role: name: borgbase.ansible_role_borgbackup vars: borg_source_directories: @@ -8,7 +8,7 @@ - "/etc" borg_exclude_patterns: - "/home/anatoli" - - "/home/clp" + - "/home/clp" - "/home/mysql" - "*.pyc" - "*.tmp" @@ -39,5 +39,5 @@ name: borgmatic.timer state: started enabled: true - daemon_reload: yes + daemon_reload: true become: true diff --git a/web3/ansible/roles/system/config/handlers/main.yml b/web3/ansible/roles/system/config/handlers/main.yml index f3838ef7..9bb4b1c6 100644 --- a/web3/ansible/roles/system/config/handlers/main.yml +++ b/web3/ansible/roles/system/config/handlers/main.yml @@ -9,7 +9,7 @@ ansible.builtin.systemd_service: name: logrotate.timer state: restarted - daemon_reload: yes + daemon_reload: true become: true - name: Reload nginx diff --git a/web3/ansible/roles/system/config/tasks/main.yml b/web3/ansible/roles/system/config/tasks/main.yml index e71c35c2..3d9755d1 100644 --- a/web3/ansible/roles/system/config/tasks/main.yml +++ b/web3/ansible/roles/system/config/tasks/main.yml @@ -1,89 +1,89 @@ --- - name: UFW block: - - name: Allow incoming UDP port 7844 - community.general.ufw: - rule: allow - proto: udp - from_port: 7844 - comment: Allow incoming traffic from UDP port 7844 - delete: true - become: true + - name: Allow incoming UDP port 7844 + community.general.ufw: + rule: allow + proto: udp + from_port: 7844 + comment: Allow incoming traffic from UDP port 7844 + delete: true + become: true - name: DNS block: - - name: Ensure systemd-resolved is running - ansible.builtin.systemd_service: - name: systemd-resolved - state: started - enabled: yes - become: true + - name: Ensure systemd-resolved is running + ansible.builtin.systemd_service: + name: systemd-resolved + state: started + enabled: true + become: true - - name: Configure systemd-resolved for Google and Quad9 DNS - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?DNS=' - line: 'DNS=8.8.8.8 8.8.4.4 9.9.9.9 149.112.112.112' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for Google and Quad9 DNS + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?DNS=" + line: "DNS=8.8.8.8 8.8.4.4 9.9.9.9 149.112.112.112" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for ControlD and Cloudflare Fallback DNS - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?FallbackDNS=' - line: 'FallbackDNS=76.76.2.0 76.76.10.0 1.1.1.1 1.0.0.1' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for ControlD and Cloudflare Fallback DNS + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?FallbackDNS=" + line: "FallbackDNS=76.76.2.0 76.76.10.0 1.1.1.1 1.0.0.1" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for DNSOverTLS - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?DNSOverTLS=' - line: 'DNSOverTLS=no' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for DNSOverTLS + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?DNSOverTLS=" + line: "DNSOverTLS=no" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for Cache - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?Cache=' - line: 'Cache=yes' - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for Cache + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?Cache=" + line: "Cache=yes" + notify: Restart systemd-resolved + become: true - - name: Configure systemd-resolved for Domain - ansible.builtin.lineinfile: - path: /etc/systemd/resolved.conf - regexp: '^#?Domain=' - line: "Domain={{ dns_search_domain }}" - when: dns_search_domain | default('') | length > 0 - notify: Restart systemd-resolved - become: true + - name: Configure systemd-resolved for Domain + ansible.builtin.lineinfile: + path: /etc/systemd/resolved.conf + regexp: "^#?Domain=" + line: "Domain={{ dns_search_domain }}" + when: dns_search_domain | default('') | length > 0 + notify: Restart systemd-resolved + become: true - name: Logrotate block: - - name: Remove obsolete Docker logrotate configuration - ansible.builtin.file: - path: /etc/logrotate.d/docker - state: absent - become: true + - name: Remove obsolete Docker logrotate configuration + ansible.builtin.file: + path: /etc/logrotate.d/docker + state: absent + become: true - - name: Create logrotate.timer.d directory - ansible.builtin.file: - path: /etc/systemd/system/logrotate.timer.d - state: directory - mode: '0755' - become: true + - name: Create logrotate.timer.d directory + ansible.builtin.file: + path: /etc/systemd/system/logrotate.timer.d + state: directory + mode: "0755" + become: true - - name: Override logrotate.timer - ansible.builtin.copy: - src: files/logrotate.timer.d/override.conf - dest: /etc/systemd/system/logrotate.timer.d/override.conf - owner: root - group: root - mode: '0644' - notify: Restart logrotate.timer - become: true + - name: Override logrotate.timer + ansible.builtin.copy: + src: files/logrotate.timer.d/override.conf + dest: /etc/systemd/system/logrotate.timer.d/override.conf + owner: root + group: root + mode: "0644" + notify: Restart logrotate.timer + become: true - name: Nginx Compression # CloudPanel pre-loads brotli via /etc/nginx/modules-enabled/50-mod-ngx-brotli.conf @@ -95,32 +95,38 @@ - name: Tune gzip_comp_level ansible.builtin.replace: path: /etc/nginx/nginx.conf - regexp: '^(\s*)gzip_comp_level\s+\d+\s*;' - replace: '\g<1>gzip_comp_level 5;' + regexp: "^(\\s*)gzip_comp_level\\s+\\d+\\s*;" + replace: "\\g<1>gzip_comp_level 5;" notify: Reload nginx become: true - name: Tune gzip_types ansible.builtin.replace: path: /etc/nginx/nginx.conf - regexp: '^(\s*)gzip_types[^;]*;' - replace: '\g<1>gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml application/xml+rss application/atom+xml application/rss+xml application/wasm application/manifest+json image/svg+xml font/ttf font/otf application/vnd.ms-fontobject;' + regexp: "^(\\s*)gzip_types[^;]*;" + replace: >- + \g<1>gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml application/xml+rss + application/atom+xml application/rss+xml application/wasm application/manifest+json image/svg+xml font/ttf font/otf + application/vnd.ms-fontobject; notify: Reload nginx become: true - name: Tune brotli_comp_level ansible.builtin.replace: path: /etc/nginx/nginx.conf - regexp: '^(\s*)brotli_comp_level\s+\d+\s*;' - replace: '\g<1>brotli_comp_level 6;' + regexp: "^(\\s*)brotli_comp_level\\s+\\d+\\s*;" + replace: "\\g<1>brotli_comp_level 6;" notify: Reload nginx become: true - name: Tune brotli_types ansible.builtin.replace: path: /etc/nginx/nginx.conf - regexp: '^(\s*)brotli_types[^;]*;' - replace: '\g<1>brotli_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml application/xml+rss application/atom+xml application/rss+xml application/wasm application/manifest+json image/svg+xml font/ttf font/otf application/vnd.ms-fontobject;' + regexp: "^(\\s*)brotli_types[^;]*;" + replace: >- + \g<1>brotli_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml application/xml+rss + application/atom+xml application/rss+xml application/wasm application/manifest+json image/svg+xml font/ttf font/otf + application/vnd.ms-fontobject; notify: Reload nginx become: true diff --git a/web3/ansible/roles/system/containers/tasks/authentik.yml b/web3/ansible/roles/system/containers/tasks/authentik.yml index c31f4191..9a803b2a 100644 --- a/web3/ansible/roles/system/containers/tasks/authentik.yml +++ b/web3/ansible/roles/system/containers/tasks/authentik.yml @@ -1,18 +1,18 @@ --- - name: Create authentik directories - file: + ansible.builtin.file: path: "/opt/containers/authentik/{{ item }}" state: directory owner: "1000" group: "1000" - mode: '0755' + mode: "0755" loop: - media - custom-templates - certs - name: Check if Authentik .env file exists - stat: + ansible.builtin.stat: path: /opt/containers/authentik/.env register: authentik_env_file @@ -25,6 +25,6 @@ when: authentik_env_file.stat.exists - name: Show message when .env file is missing - debug: + ansible.builtin.debug: msg: "Authentik .env file is missing. Please create it manually at /opt/containers/authentik/.env" when: not authentik_env_file.stat.exists diff --git a/web3/ansible/roles/system/containers/tasks/deploy_stack.yml b/web3/ansible/roles/system/containers/tasks/deploy_stack.yml index 6562c31e..2957bb53 100644 --- a/web3/ansible/roles/system/containers/tasks/deploy_stack.yml +++ b/web3/ansible/roles/system/containers/tasks/deploy_stack.yml @@ -9,7 +9,7 @@ path: "/opt/containers/{{ item.key }}/.env" owner: root group: root - mode: '0600' + mode: "0600" when: item.value.env_file and env_stat.stat.exists - name: Deploy stack diff --git a/web3/ansible/roles/system/containers/tasks/fleet.yml b/web3/ansible/roles/system/containers/tasks/fleet.yml index 0baf6376..6ac68ca0 100644 --- a/web3/ansible/roles/system/containers/tasks/fleet.yml +++ b/web3/ansible/roles/system/containers/tasks/fleet.yml @@ -1,6 +1,6 @@ --- - name: Check if fleet .env file exists - stat: + ansible.builtin.stat: path: /opt/containers/fleet/.env register: fleet_env_stat @@ -8,4 +8,4 @@ community.docker.docker_compose_v2: project_src: "/opt/containers/fleet" state: present - when: fleet_env_stat.stat.exists \ No newline at end of file + when: fleet_env_stat.stat.exists diff --git a/web3/ansible/roles/system/containers/tasks/main.yml b/web3/ansible/roles/system/containers/tasks/main.yml index 7c423eeb..b28d8894 100644 --- a/web3/ansible/roles/system/containers/tasks/main.yml +++ b/web3/ansible/roles/system/containers/tasks/main.yml @@ -1,19 +1,19 @@ --- - name: Ensure /opt/containers exists - file: + ansible.builtin.file: path: /opt/containers state: directory owner: root group: root - mode: '0755' + mode: "0755" - name: Copy containers directory to remote node - copy: + ansible.builtin.copy: src: "{{ playbook_dir }}/../containers/" dest: /opt/containers/ owner: root group: root - mode: '0755' + mode: "0755" notify: Restart containers - name: Create shared app-infra Docker network @@ -26,30 +26,30 @@ state: present - name: Create cloudflared .env file - copy: + ansible.builtin.copy: content: | TUNNEL_TOKEN={{ lookup('env', 'CLOUDFLARED_TOKEN') }} dest: /opt/containers/cloudflared/.env owner: root group: root - mode: '0600' + mode: "0600" when: lookup('env', 'CLOUDFLARED_TOKEN') | length > 0 - name: Setup Authentik Infrastructure - include_tasks: authentik.yml + ansible.builtin.include_tasks: authentik.yml - name: Setup Fleet Infrastructure - include_tasks: fleet.yml + ansible.builtin.include_tasks: fleet.yml - name: Deploy docker-compose projects - include_tasks: deploy_stack.yml + ansible.builtin.include_tasks: deploy_stack.yml loop: "{{ docker_stacks | dict2items }}" - name: Clean up Docker system community.docker.docker_prune: - containers: yes - images: yes + containers: true + images: true images_filters: dangling: false - networks: yes - builder_cache: yes + networks: true + builder_cache: true diff --git a/web3/ansible/roles/system/crowdsec/handlers/main.yml b/web3/ansible/roles/system/crowdsec/handlers/main.yml index 50521d26..fd206978 100644 --- a/web3/ansible/roles/system/crowdsec/handlers/main.yml +++ b/web3/ansible/roles/system/crowdsec/handlers/main.yml @@ -1,9 +1,10 @@ +--- - name: restart crowdsec - service: + ansible.builtin.service: name: crowdsec state: restarted - name: restart crowdsec-firewall-bouncer - service: + ansible.builtin.service: name: crowdsec-firewall-bouncer - state: restarted \ No newline at end of file + state: restarted diff --git a/web3/ansible/roles/system/crowdsec/tasks/main.yml b/web3/ansible/roles/system/crowdsec/tasks/main.yml index 060d0b6b..4110c0d7 100644 --- a/web3/ansible/roles/system/crowdsec/tasks/main.yml +++ b/web3/ansible/roles/system/crowdsec/tasks/main.yml @@ -1,12 +1,13 @@ +--- - name: Read CrowdSec token from environment - set_fact: + ansible.builtin.set_fact: crowdsec_token: "{{ lookup('env', 'CROWDSEC_TOKEN') | default('', true) }}" - name: Download CrowdSec GPG key - get_url: + ansible.builtin.get_url: url: https://packagecloud.io/crowdsec/crowdsec/gpgkey dest: /usr/share/keyrings/crowdsec.asc - mode: '0644' + mode: "0644" - name: Install deb822 repository dependency ansible.builtin.apt: @@ -38,7 +39,7 @@ - name: Update apt cache ansible.builtin.apt: - update_cache: yes + update_cache: true when: crowdsec_repo.changed - name: Install CrowdSec @@ -57,54 +58,56 @@ state: present - name: Deploy CrowdSec configuration - template: + ansible.builtin.template: src: config.yaml.j2 dest: /etc/crowdsec/config.yaml + mode: "0644" notify: restart crowdsec - name: Update local API credentials URL - lineinfile: + ansible.builtin.lineinfile: path: /etc/crowdsec/local_api_credentials.yaml - regexp: '^url:' - line: 'url: http://127.0.0.1:8081' + regexp: "^url:" + line: "url: http://127.0.0.1:8081" - name: Enable and start CrowdSec service - service: + ansible.builtin.service: name: crowdsec - enabled: yes + enabled: true state: started - name: Enable and start CrowdSec firewall bouncer service - service: + ansible.builtin.service: name: crowdsec-firewall-bouncer - enabled: yes + enabled: true state: started - name: Update firewall bouncer API URL - lineinfile: + ansible.builtin.lineinfile: path: /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml - regexp: '^api_url:' - line: 'api_url: http://127.0.0.1:8081/' + regexp: "^api_url:" + line: "api_url: http://127.0.0.1:8081/" notify: restart crowdsec-firewall-bouncer - name: Ensure CrowdSec firewall bouncer uses nftables mode - lineinfile: + ansible.builtin.lineinfile: path: /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml - regexp: '^mode:' - line: 'mode: nftables' + regexp: "^mode:" + line: "mode: nftables" notify: restart crowdsec-firewall-bouncer - name: Update nginx bouncer API URL - lineinfile: + ansible.builtin.lineinfile: path: /etc/crowdsec/bouncers/crowdsec-nginx-bouncer.conf - regexp: '^API_URL=' - line: 'API_URL=http://127.0.0.1:8081' + regexp: "^API_URL=" + line: "API_URL=http://127.0.0.1:8081" - name: Check CrowdSec console enrollment status - command: cscli console status -o json + ansible.builtin.command: cscli console status -o json register: console_status changed_when: false - name: Enroll CrowdSec with console - command: cscli console enroll "{{ crowdsec_token }}" - when: crowdsec_token | length > 0 and 'console_management' not in (console_status.stdout | from_json) \ No newline at end of file + ansible.builtin.command: cscli console enroll "{{ crowdsec_token }}" + changed_when: true + when: crowdsec_token | length > 0 and 'console_management' not in (console_status.stdout | from_json) diff --git a/web3/ansible/roles/system/migrations/defaults/main.yml b/web3/ansible/roles/system/migrations/defaults/main.yml index 25427a89..94346280 100644 --- a/web3/ansible/roles/system/migrations/defaults/main.yml +++ b/web3/ansible/roles/system/migrations/defaults/main.yml @@ -1,3 +1,3 @@ --- migration_dir: "{{ playbook_dir }}/migrations" -migration_state_file: /opt/ansible/migrations.db \ No newline at end of file +migration_state_file: /opt/ansible/migrations.db diff --git a/web3/ansible/roles/system/migrations/tasks/main.yml b/web3/ansible/roles/system/migrations/tasks/main.yml index 5d801ce6..addef147 100644 --- a/web3/ansible/roles/system/migrations/tasks/main.yml +++ b/web3/ansible/roles/system/migrations/tasks/main.yml @@ -1,61 +1,63 @@ --- - name: Ensure sqlite3 is installed - package: + ansible.builtin.package: name: sqlite3 state: present - name: Ensure /opt/ansible directory exists - file: + ansible.builtin.file: path: /opt/ansible state: directory - mode: '0755' + mode: "0755" - name: Ensure migration state database exists - command: sqlite3 {{ migration_state_file }} "CREATE TABLE IF NOT EXISTS migrations (id TEXT PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP);" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "CREATE TABLE IF NOT EXISTS migrations (id TEXT PRIMARY KEY, applied_at DATETIME DEFAULT + CURRENT_TIMESTAMP);" args: creates: "{{ migration_state_file }}" - name: Get applied migrations - command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" register: applied_migrations_output changed_when: false - name: Parse applied migrations - set_fact: + ansible.builtin.set_fact: applied_migrations: "{{ applied_migrations_output.stdout_lines | select | list }}" - name: Show applied migrations - debug: + ansible.builtin.debug: msg: "Already applied migrations: {{ applied_migrations }}" verbosity: 1 no_log: false - name: Find migration files - find: + ansible.builtin.find: paths: "{{ migration_dir }}" patterns: "*.yml" - recurse: no + recurse: false delegate_to: localhost register: migration_files - name: Extract migration IDs from files - set_fact: + ansible.builtin.set_fact: pending_migrations: | {{ migration_files.files | map(attribute='path') | map('basename') | map('regex_replace', '\.yml$', '') | reject('in', applied_migrations) | list }} - name: Show pending migrations - debug: + ansible.builtin.debug: msg: "Pending migrations to apply: {{ pending_migrations }}" verbosity: 1 no_log: false - name: Run pending migrations - include_tasks: "{{ migration_dir }}/{{ item }}.yml" + ansible.builtin.include_tasks: "{{ migration_dir }}/{{ item }}.yml" loop: "{{ pending_migrations }}" register: migration_results - name: Record applied migrations - command: sqlite3 {{ migration_state_file }} "INSERT INTO migrations (id) VALUES ('{{ item }}');" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "INSERT INTO migrations (id) VALUES ('{{ item }}');" + changed_when: true loop: "{{ pending_migrations }}" loop_control: index_var: migration_index @@ -65,12 +67,12 @@ - not (migration_results.results[migration_index].failed | default(false)) - name: Verify migrations were recorded - command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" + ansible.builtin.command: sqlite3 {{ migration_state_file }} "SELECT id FROM migrations;" register: final_migrations changed_when: false - name: Show all recorded migrations - debug: + ansible.builtin.debug: msg: "All recorded migrations: {{ final_migrations.stdout_lines }}" verbosity: 1 - no_log: false \ No newline at end of file + no_log: false diff --git a/web3/ansible/roles/system/monit/tasks/main.yml b/web3/ansible/roles/system/monit/tasks/main.yml index e8ec75b1..5b2139c1 100644 --- a/web3/ansible/roles/system/monit/tasks/main.yml +++ b/web3/ansible/roles/system/monit/tasks/main.yml @@ -13,7 +13,7 @@ state: directory owner: root group: root - mode: '0755' + mode: "0755" loop: - /etc/monit - /etc/monit.d @@ -25,7 +25,7 @@ dest: /etc/monitrc owner: root group: root - mode: '0600' + mode: "0600" notify: Reload monit become: true @@ -35,7 +35,7 @@ dest: /etc/monit.d/disk.cfg owner: root group: root - mode: '0600' + mode: "0600" notify: Reload monit become: true @@ -45,7 +45,7 @@ dest: /etc/monit/notify-chat.sh owner: root group: root - mode: '0755' + mode: "0755" notify: Reload monit become: true @@ -55,7 +55,7 @@ dest: /etc/monit/check-docker-stack.sh owner: root group: root - mode: '0755' + mode: "0755" notify: Reload monit become: true @@ -65,7 +65,7 @@ dest: /etc/monit.d/containers.cfg owner: root group: root - mode: '0600' + mode: "0600" notify: Reload monit become: true @@ -75,7 +75,7 @@ dest: /etc/monit/notify-chat.env owner: root group: root - mode: '0600' + mode: "0600" force: false become: true @@ -85,7 +85,7 @@ dest: /etc/monit.d/mail.cfg owner: root group: root - mode: '0600' + mode: "0600" force: false become: true diff --git a/web3/ansible/roles/system/php/tasks/_build_php_extension.yml b/web3/ansible/roles/system/php/tasks/_build_php_extension.yml index 39226ecd..69c93342 100644 --- a/web3/ansible/roles/system/php/tasks/_build_php_extension.yml +++ b/web3/ansible/roles/system/php/tasks/_build_php_extension.yml @@ -29,13 +29,13 @@ ansible.builtin.file: path: "/tmp/php-ext-build/{{ php_version }}" state: directory - mode: '0755' + mode: "0755" - name: "PHP {{ php_version }} {{ ext.name }} - download PECL tarball" ansible.builtin.get_url: url: "https://pecl.php.net/get/{{ ext.name }}-{{ ext.version }}.tgz" dest: "/tmp/php-ext-build/{{ ext.name }}-{{ ext.version }}.tgz" - mode: '0644' + mode: "0644" - name: "PHP {{ php_version }} {{ ext.name }} - extract source into version-scoped directory" ansible.builtin.unarchive: @@ -54,6 +54,7 @@ args: chdir: "/tmp/php-ext-build/{{ php_version }}/{{ ext.name }}-{{ ext.version }}" executable: /bin/bash + changed_when: true - name: "PHP {{ php_version }} {{ ext.name }} - register in cli php.ini" ansible.builtin.lineinfile: diff --git a/web3/ansible/roles/system/php/tasks/main.yml b/web3/ansible/roles/system/php/tasks/main.yml index edf3390d..4fd6cf58 100644 --- a/web3/ansible/roles/system/php/tasks/main.yml +++ b/web3/ansible/roles/system/php/tasks/main.yml @@ -3,7 +3,7 @@ ansible.builtin.systemd_service: name: "php{{ item }}-fpm" state: stopped - enabled: no + enabled: false loop: - "7.1" - "7.2" @@ -18,7 +18,7 @@ ansible.builtin.file: path: "/etc/php/{{ item }}/fpm/pool.d" mode: "a+r" - recurse: yes + recurse: true loop: - "7.1" - "7.2" diff --git a/web3/containers/1min-relay/docker-compose.yml b/web3/containers/1min-relay/docker-compose.yml index f652da2d..35273444 100644 --- a/web3/containers/1min-relay/docker-compose.yml +++ b/web3/containers/1min-relay/docker-compose.yml @@ -1,3 +1,4 @@ +--- services: 1min-relay: image: ghcr.io/thundersquared/1min-relay:v1.1.0 diff --git a/web3/containers/calcom/docker-compose.yml b/web3/containers/calcom/docker-compose.yml index b354a0a9..d616e6d5 100644 --- a/web3/containers/calcom/docker-compose.yml +++ b/web3/containers/calcom/docker-compose.yml @@ -1,3 +1,4 @@ +--- services: database: image: postgres:18-alpine diff --git a/web3/containers/cloudflared/docker-compose.yml b/web3/containers/cloudflared/docker-compose.yml index 82fd5d41..83f75c94 100644 --- a/web3/containers/cloudflared/docker-compose.yml +++ b/web3/containers/cloudflared/docker-compose.yml @@ -1,3 +1,4 @@ +--- services: cloudflared: image: cloudflare/cloudflared:2026.9.3@sha256:072c067d25ccbe61d46e18f0d0723255f2bb5304f7317caa95b27031520ff92c diff --git a/web3/containers/cobalt/docker-compose.yml b/web3/containers/cobalt/docker-compose.yml index d609cfe7..ad58a9fb 100644 --- a/web3/containers/cobalt/docker-compose.yml +++ b/web3/containers/cobalt/docker-compose.yml @@ -1,3 +1,4 @@ +--- services: cobalt-api: image: ghcr.io/imputnet/cobalt:11.7.1 diff --git a/web3/containers/fleet/docker-compose.yml b/web3/containers/fleet/docker-compose.yml index 2bf0d46b..fc37b789 100644 --- a/web3/containers/fleet/docker-compose.yml +++ b/web3/containers/fleet/docker-compose.yml @@ -1,3 +1,4 @@ +--- services: database: image: mysql:8.4 diff --git a/web3/containers/gpt-load/docker-compose.yml b/web3/containers/gpt-load/docker-compose.yml index 3bbc4f64..e6aaa60e 100644 --- a/web3/containers/gpt-load/docker-compose.yml +++ b/web3/containers/gpt-load/docker-compose.yml @@ -1,7 +1,7 @@ --- services: gpt-load: - image: ghcr.io/tbphp/gpt-load:2.0.0-rc.33@sha256:c9c38c606d0832b46fd905b34687d668ec737bff80d63199b9beab4bb0c4a832 # yamllint disable-line rule:line-length + image: ghcr.io/tbphp/gpt-load:2.0.0-rc.33@sha256:c9c38c606d0832b46fd905b34687d668ec737bff80d63199b9beab4bb0c4a832 # yamllint disable-line rule:line-length restart: unless-stopped init: true cap_drop: @@ -27,7 +27,6 @@ services: volumes: gpt-load-data: - networks: app-infra: external: true diff --git a/web3/containers/hermes/docker-compose.yml b/web3/containers/hermes/docker-compose.yml index 769fa3e5..778cb9f7 100644 --- a/web3/containers/hermes/docker-compose.yml +++ b/web3/containers/hermes/docker-compose.yml @@ -2,7 +2,7 @@ services: gateway: # renovate: datasource=docker depName=nousresearch/hermes-agent - image: nousresearch/hermes-agent:v2026.9.24@sha256:fca358f12efd65bfaaca05884166f15c0e2788375ca30d77061ac1ebc96452b7 # yamllint disable-line rule:line-length + image: nousresearch/hermes-agent:v2026.9.24@sha256:fca358f12efd65bfaaca05884166f15c0e2788375ca30d77061ac1ebc96452b7 # yamllint disable-line rule:line-length command: ["gateway", "run"] env_file: - .env diff --git a/web3/containers/n8n/docker-compose.yml b/web3/containers/n8n/docker-compose.yml index 5d8a2f44..9edefb6d 100644 --- a/web3/containers/n8n/docker-compose.yml +++ b/web3/containers/n8n/docker-compose.yml @@ -58,7 +58,6 @@ services: volumes: postgres: n8n_data: - networks: n8n: app-infra: diff --git a/web3/containers/open-webui/docker-compose.yml b/web3/containers/open-webui/docker-compose.yml index 40bbf7c9..9db9c670 100644 --- a/web3/containers/open-webui/docker-compose.yml +++ b/web3/containers/open-webui/docker-compose.yml @@ -1,3 +1,4 @@ +--- services: open-webui: image: ghcr.io/open-webui/open-webui:v0.11.4@sha256:9591b13f13843c7721c2b8eaf7382846c81b3ffe126526d1888d1fed50c6a33f @@ -17,7 +18,6 @@ services: volumes: open_webui: - networks: app-infra: external: true diff --git a/web3/containers/twenty/docker-compose.yml b/web3/containers/twenty/docker-compose.yml index 03769b10..2600a810 100644 --- a/web3/containers/twenty/docker-compose.yml +++ b/web3/containers/twenty/docker-compose.yml @@ -90,7 +90,6 @@ services: volumes: db: server-local-data: - networks: twenty: app-infra: