From 3882b6035fdc737230718dfadb85bbd8ab32e483 Mon Sep 17 00:00:00 2001 From: GGThed Date: Tue, 11 Aug 2026 14:42:23 -0400 Subject: [PATCH] fix(ops): defauts surs a la copie, CDN epingles, actions epinglees MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quatre taches de la matrice du rapport, toutes sans dependance, qu aucune liste de « ce qui reste » ne reprenait. OPS-003 — app/.env.exemple disait « copiez ce fichier et remplissez les valeurs pour la production », puis posait FLASK_DEBUG=true, SESSION_COOKIE_SECURE=false et FORCE_HTTPS=false. Le debogueur Werkzeug execute du code soumis par le navigateur : cette ligne transformait un copier-coller en shell distant. Chaque valeur est desormais sure a la copie, et le fichier refuse de demarrer tant que les deux secrets obligatoires ne sont pas remplis plutot que de demarrer grand ouvert. Renomme en .env.example : l orthographe francaise ne correspondait pas a l exception !.env.example du .gitignore, donc le fichier n etait suivi que par accident de l ordre des regles. Les deux points de la decision ouverte du §8 tombent d un seul git mv. OPS-002 — trusted_proxy='*' et HOST ne sont plus soudes dans wsgi.py. Les defauts sont **inchanges**, deliberement : choisir sans connaitre la topologie coupe la prod si nginx est ailleurs, ou casse la limitation de debit pour tout le monde si on cesse de croire X-Forwarded-For alors que c etait la seule source d adresses. Ce sont maintenant des variables, les valeurs sures sont dans .env.example pour un nouveau deploiement, et docs/deployment.md donne les quatre topologies avec la valeur de chacune. wsgi.py avertit au demarrage tant que les deux defauts sont en place. Le commentaire de HOST annoncait « bind to localhost by default » a cote d un defaut a 0.0.0.0 : il decrivait l intention pendant que le code faisait l inverse. Il dit maintenant ce qu il fait. QUA-004 — Font Awesome et FullCalendar etaient charges sans empreinte, depuis des hotes que la CSP autorise nommement. Qui controle ces CDN controlait ce qui s execute sur chaque page. Empreintes posees, avec ce que SRI promet et ce qu il ne promet pas ecrit a cote : ca fige le fichier, ca ne prouve pas qu il etait honnete au moment du calcul. **Le CSS de FullCalendar n existait pas.** La v6 embarque ses styles dans le JS et ce fichier n est pas publie : le repondait 404 a chaque ouverture du calendrier depuis la montee de version. Une feuille de style en echec est silencieuse dans le navigateur, c est ce qui l a fait durer. CI-003 — actions epinglees sur un commit, version en commentaire, dans les deux forges. Un tag est un pointeur mobile : deplacer v4 fait executer du code arbitraire dans le job qui detient la cle SSH de production. Ce job recoit aussi enfin un bloc permissions. 517 tests. --- .gitea/workflows/git-to-ptero.yaml | 15 +++- .github/workflows/ci.yml | 22 ++++-- README.md | 2 +- app/.env.example | 115 ++++++++++++++++++++++++++++ app/.env.exemple | 34 -------- app/templates/layouts/base.html | 17 +++- app/templates/pages/calendar.html | 15 +++- docs/deployment.md | 46 +++++++++++ tests/test_subresource_integrity.py | 104 +++++++++++++++++++++++++ wsgi.py | 86 +++++++++++++++++++-- 10 files changed, 401 insertions(+), 55 deletions(-) create mode 100644 app/.env.example delete mode 100644 app/.env.exemple create mode 100644 tests/test_subresource_integrity.py diff --git a/.gitea/workflows/git-to-ptero.yaml b/.gitea/workflows/git-to-ptero.yaml index 3489c64..3ff6e79 100644 --- a/.gitea/workflows/git-to-ptero.yaml +++ b/.gitea/workflows/git-to-ptero.yaml @@ -34,6 +34,17 @@ on: # # docs/deployment.md carries the design and the rollback procedure. +# Least privilege (CI-003). This job never writes back to the repository; it +# holds the SSH key to the production node, which makes it the most valuable +# job in either forge to compromise. +permissions: + contents: read + +# Actions pinned to a commit, version in the comment. A tag is a moving +# pointer, and moving `v4` here means running arbitrary code in the job that +# holds that key. If the Gitea runner ever fails to resolve a commit ref, it +# fails on the checkout step — loudly, like the `@v7` that did not exist. + jobs: deploy-to-sftp: runs-on: ubuntu-latest @@ -42,10 +53,10 @@ jobs: # Was @v7, which does not exist (latest major is v5): the workflow # failed on its very first step. - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: '3.12' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6fce70..95d343e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,12 @@ concurrency: permissions: contents: read +# Third-party actions are pinned to a commit, with the version in a comment +# (CI-003). A tag is a moving pointer: whoever can move `v4` runs code in a +# job that holds this repository's token. The comment is what makes the pin +# maintainable — a bare 40-character hash tells a reader nothing about +# whether it is current. Dependabot updates both together. + env: PYTHON_VERSION: '3.12' @@ -23,10 +29,10 @@ jobs: name: Security Audit runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: ${{ env.PYTHON_VERSION }} cache: 'pip' @@ -46,10 +52,10 @@ jobs: name: Lint with Ruff runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: ${{ env.PYTHON_VERSION }} @@ -71,10 +77,10 @@ jobs: name: Security Scan runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: ${{ env.PYTHON_VERSION }} cache: 'pip' @@ -96,10 +102,10 @@ jobs: runs-on: ubuntu-latest needs: [security-audit, lint] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: ${{ env.PYTHON_VERSION }} cache: 'pip' diff --git a/README.md b/README.md index f49ee07..1c512a2 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ de la barre latérale (voir `docs/translations.md`). ```bash python -m venv .venv .venv/Scripts/pip install -r requirements.txt -r requirements-dev.txt -cp app/.env.exemple .env # puis remplir les valeurs +cp app/.env.example .env # puis remplir SECRET_KEY et DATABASE_URL .venv/Scripts/python run.py # développement, http://127.0.0.1:5000 ``` diff --git a/app/.env.example b/app/.env.example new file mode 100644 index 0000000..e6cc4d2 --- /dev/null +++ b/app/.env.example @@ -0,0 +1,115 @@ +# Team Tryouts — environment variables +# +# Copy to .env and fill in. Every value here is a PRODUCTION-SAFE default: +# copying this file and changing nothing gives a locked-down configuration +# that refuses to start until the two required secrets are set, rather than +# a working one that happens to be wide open (OPS-003). +# +# The previous version shipped FLASK_DEBUG=true under a heading that said +# "fill in the values for production". The Werkzeug debugger executes code +# submitted through the browser, so that one line turned a copy-paste into a +# remote shell. +# +# For local development, see the DEVELOPMENT block at the bottom. + +# ============================================================================= +# Required — the application refuses to start without these +# ============================================================================= + +# Generate with: python -c "import secrets; print(secrets.token_hex(32))" +# Never reuse one between environments: this key signs session cookies, so +# whoever holds it can forge a session for any account. +SECRET_KEY= + +# Expected form: postgresql://user:password@host:5432/database +# The psycopg 3 driver is named for you by create_app(); postgresql:// alone +# would send SQLAlchemy looking for psycopg2, which is not installed. +DATABASE_URL= + +# ============================================================================= +# Security — these defaults assume HTTPS in front. Do not relax them on a +# deployed instance. +# ============================================================================= + +# Session cookies are only sent over HTTPS. +SESSION_COOKIE_SECURE=true + +# Plain HTTP is redirected to HTTPS. +FORCE_HTTPS=true + +# The Werkzeug debugger is a remote code execution primitive by design. +# Never true on anything reachable from a network you do not control. +FLASK_DEBUG=false + +# Inline +{# The stylesheet that used to sit here — index.global.min.css — does not + exist. FullCalendar 6 bundles its styles into the JS, and that file is not + in the published package: the link had been answering 404 on every calendar + load since the upgrade. A failed stylesheet is silent in the browser, which + is why it survived. + + Integrity pins the bundle (QUA-004): this file is executable script from a + third party, on the page that shows every match in the club. See the note + in layouts/base.html for what SRI does and does not promise. #} +