Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .ansible-lint
Original file line number Diff line number Diff line change
@@ -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"
43 changes: 43 additions & 0 deletions .github/actions/setup-lint-env/action.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .github/workflows/mirror-ansible-deploy.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
name: Deploy mirror with Ansible
permissions:
contents: read
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/mirror-tofu-apply.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
name: Deploy mirror with OpenTofu
permissions:
contents: read
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/mx1-ansible-deploy.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
name: Deploy mx1 with Ansible
permissions:
contents: read
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/tower-ansible-deploy.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
name: Deploy tower with Ansible
permissions:
contents: read
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/tower-tofu-apply.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
name: Deploy tower with OpenTofu
permissions:
contents: read
Expand Down
150 changes: 150 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .github/workflows/web1-ansible-deploy.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
name: Deploy web1 with Ansible
permissions:
contents: read
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/web2-ansible-deploy.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
name: Deploy web2 with Ansible
permissions:
contents: read
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/web3-ansible-deploy.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
name: Deploy web3 with Ansible
permissions:
contents: read
Expand Down
55 changes: 55 additions & 0 deletions .yamllint
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading