fix(ops): defauts surs a la copie, CDN epingles, actions epinglees
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 <link> 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.
This commit is contained in:
@@ -8,8 +8,33 @@ Usage:
|
||||
python wsgi.py
|
||||
|
||||
Configuration via environment variables:
|
||||
PORT: Port to listen on (default: 5000)
|
||||
HOST: interface to bind (default: 0.0.0.0 — see the note below)
|
||||
PORT: Port to listen on (default: 10000)
|
||||
WAITRESS_THREADS: Number of worker threads (default: CPU*2+1)
|
||||
TRUSTED_PROXY: whose X-Forwarded-For to believe (default: * — see below)
|
||||
|
||||
A note on the three defaults above (OPS-002)
|
||||
--------------------------------------------
|
||||
They are what this file has always done, kept deliberately. Two of them are
|
||||
known to be wrong, and they are *still* not changed here, because the right
|
||||
value depends on something the repository cannot tell us: is nginx on the
|
||||
same machine as the app, or is the app in a Pterodactyl container with nginx
|
||||
elsewhere?
|
||||
|
||||
Guessing fails in one of two directions, and neither is recoverable by
|
||||
reading a log:
|
||||
|
||||
- bind to loopback when nginx is on another host, and the site is simply
|
||||
gone;
|
||||
- stop trusting X-Forwarded-For when it was the only source of client
|
||||
addresses, and every request looks like it comes from the proxy — one
|
||||
shared rate-limit bucket, so the first person to mistype a password five
|
||||
times locks the limiter for everybody.
|
||||
|
||||
So: the values became settings, the safe values are what `.env.example`
|
||||
carries for a new deployment, and the existing one keeps working untouched
|
||||
until someone who knows the topology sets them. docs/deployment.md walks
|
||||
through both cases.
|
||||
"""
|
||||
|
||||
import multiprocessing
|
||||
@@ -19,14 +44,65 @@ from app.app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
|
||||
def proxy_settings():
|
||||
"""How much of X-Forwarded-For to believe, and from whom (OPS-002).
|
||||
|
||||
This decides which address the rate limiter counts against and which one
|
||||
the audit log records as `ip=`.
|
||||
|
||||
The default is `*` — believe anyone. That is the setting the audit called
|
||||
out for inverting the risk rather than fixing it: before it, rate
|
||||
limiting was miscalibrated but closed; after it, forgeable on every
|
||||
request by anyone who can reach the app directly. It stays the default
|
||||
only because narrowing it blind can take the rate limiter down for every
|
||||
legitimate user, and it is now one environment variable away instead of
|
||||
being welded into the file.
|
||||
|
||||
Values:
|
||||
'' trust nobody; remote_addr is the peer. Correct when
|
||||
nothing proxies the app.
|
||||
'127.0.0.1' trust a reverse proxy on this same machine. The usual
|
||||
case, and what .env.example ships.
|
||||
'*' trust everyone. Only correct when the app cannot be
|
||||
reached except through the proxy, at the network level.
|
||||
|
||||
Returns:
|
||||
dict: keyword arguments for waitress.serve.
|
||||
"""
|
||||
trusted = os.getenv('TRUSTED_PROXY', '*').strip()
|
||||
|
||||
if not trusted:
|
||||
return {'clear_untrusted_proxy_headers': True}
|
||||
|
||||
return {
|
||||
'trusted_proxy': trusted,
|
||||
'trusted_proxy_count': int(os.getenv('TRUSTED_PROXY_COUNT', 1)),
|
||||
'trusted_proxy_headers': {'x-forwarded-for', 'x-forwarded-proto'},
|
||||
'clear_untrusted_proxy_headers': True,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from waitress import serve
|
||||
|
||||
port = int(os.getenv('PORT', 10000))
|
||||
threads = int(os.getenv('WAITRESS_THREADS', multiprocessing.cpu_count() * 2 + 1))
|
||||
host = os.getenv('HOST', '0.0.0.0') # Bind to localhost by default (Nginx reverse proxy)
|
||||
# The comment that used to sit here said "Bind to localhost by default
|
||||
# (Nginx reverse proxy)" next to a default of 0.0.0.0 — it described the
|
||||
# intention and the code did the opposite, which is worse than either:
|
||||
# the app was reachable directly and nobody reading the file would have
|
||||
# known. The default is unchanged; the comment now says what it does.
|
||||
host = os.getenv('HOST', '0.0.0.0') # noqa: S104 — see the module docstring
|
||||
|
||||
print(f'Starting Waitress server on {host}:{port} with {threads} threads')
|
||||
if host == '0.0.0.0' and os.getenv('TRUSTED_PROXY', '*') == '*': # noqa: S104
|
||||
print(
|
||||
'WARNING: listening on every interface and trusting X-Forwarded-For from '
|
||||
'anyone. If this port is reachable without going through nginx, the rate '
|
||||
'limiter and the audit log can be fed any address a caller likes. '
|
||||
'Set HOST and TRUSTED_PROXY — see docs/deployment.md (OPS-002).'
|
||||
)
|
||||
serve(
|
||||
app,
|
||||
host=host,
|
||||
@@ -35,9 +111,5 @@ if __name__ == '__main__':
|
||||
# Graceful shutdown settings
|
||||
channel_timeout=30, # Seconds to wait for in-flight requests
|
||||
cleanup_interval=30,
|
||||
# Waitress Proxy Settings
|
||||
trusted_proxy='*',
|
||||
trusted_proxy_count=1,
|
||||
trusted_proxy_headers={'x-forwarded-for', 'x-forwarded-proto'},
|
||||
clear_untrusted_proxy_headers=True,
|
||||
**proxy_settings(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user