todo: à documenter

Service présent sur bulbe.

Procédures

Bases d'authentification

Les fichiers servant à identifier les users pour l'authentification HTTP sont regroupés dans /etc/nginx/passwd. Ces fichiers sont au même format que ceux d'Apache et sont manipulables avec la commande htpasswd du paquet apache2-utils.

Certificat X.509

Le certificat X.509 utilisé est signé par Let's Encrypt, et maintenu par acmetool.

Une de ses spécificités est qu'on utilise le drapeau OCSP-Must-Staple, qui indique au navigateur que le serveur doit fournir un message (une « agrafe »), signé par l'autorité de certification, qui confirme que le certificat n'a pas été révoqué. Ça a la joyeuse idée de permettre de révoquer ces certificats de façon fiable.

Malheureusement, Nginx a un bug de gestion de l'agrafe qui fait que la première requête après son lancement, le rechargement de sa configuration ou l'expiration de l'agrafe précédente … ne reçoit pas d'agrafe du tout.

Du coup, nicoo a contourné le problème en déclenchant une requête quand Nginx est lancé, quand sa configuration est rechargée et toutes les heures :

# /etc/systemd/system/nginx-ocsp.service
[Unit]
Description=Force Nginx to refresh its OCSP staple (if needed)

[Service]
Type=oneshot
User=nobody
ExecStart=/usr/bin/curl --silent -o /dev/null https://nos-oignons.net
# /etc/systemd/system/nginx-ocsp.timer
[Unit]
Description=Maintain Nginx's OCSP staple (periodic trigger)
Requires=nginx.service

[Timer]
OnUnitActiveSec=1h
# /etc/systemd/system/nginx.service.d/ocsp-staple.conf
[Service]
ExecStartPost=/usr/bin/curl --silent -o /dev/null https://nos-oignons.net
ExecReload=/usr/bin/curl --silent -o /dev/null https://nos-oignons.net

[Unit]
Wants=nginx-ocsp.timer

Configuration SSL

Le site de Qualys SSL Labs contient un test pour vérifier la configuration d'un serveur HTTPS. Il donne également une série de recommandations sur la meilleure configuration à adopter.

La configuration de TLS pour Nginx est dans /etc/nginx/snippets/tls.conf :

## Nginx TLS config
ssl_protocols TLSv1 TLSV1.1 TLSv1.2;

# Cf. https://nos-oignons.net/wiki-admin/Services/Web/
ssl_prefer_server_ciphers on;
ssl_ciphers ECDH+AESGCM128:ECDH+AESGCM256:ECDH+AES128:ECDH+AES256:!aNULL:!MD5:!RC4:!DSS:!EXPORT;

ssl_session_tickets off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1h;

ssl_stapling on;
ssl_stapling_verify on;
resolver 127.0.0.1;

Explication du choix de la ciphersuite

Ces dernières ont été choisies par nicoo, en s'appuyant sur les recommandations de Mozilla, Jacob Applebaum, l'EFF, Hynek Schlawack, pour les raisons suivantes :

  1. On n'autorise que la Forward Secrecy avec un échange de clefs ECDH.

  2. On préfère (dans l'ordre) :

    • AES-GCM ou AES-CBC ;
    • AES128 ou AES256 (des attaques sur le key schedule rendent AES256 pas beaucoup plus sur que la version 128 bits, mais il reste plus lent).
  3. On supporte tous les navigateurs, sauf Internet Explorer sous WinXP.

En-têtes relatifs à la sécurité

HTTP Strict Transport Security

Afin que les navigateurs se souviennent que le site de Nos oignons doit être consulté en HTTPS, on indique dans le VirtualHost HTTPS :

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload
  • La valeur de max-age vaut pour 12 mois.
  • includeSubDomains et preload sont requis pour HSTS Preload Le pré-chargement de nos-oignons.net dans les listes HSTS des navigateurs force l'utilisation de HTTPS (y compris lors de la première visite), qu'ils aient HTTPS Everywhere ou non.

Content Security Policy

Une CSP définit une politique décrivant quelles ressources la page peut charger, quelles requêtes peuvent être émises, et que faire vis-à-vis du referrer.

Ça vise à rendre difficile, voir impossible, une large famille d'attaques, en particulier les attaques XSS (Cross-Site Scripting, où un attaquant injecte du contenu dans une page, qui est typiquement interprété comme du HTML).

Malheureusement, certains applications (AWStats, Gitweb, Mailman, Request Tracker) utilisent des scripts ou des styles « en ligne », embarqués dans le corps du document, ce qui est mauvais d'un point de vue sécurité. De façon ironique, ce sont aussi les applications les plus à risque qui ont les plus mauvaises pratiques ...

Du coup, on définit deux politiques différentes :

  • une politique par défaut, stricte ;
  • une politique plus « coulante », qu'on applique au cas par cas.

Paramètres communs

La majeure partie de la politique est la même dans les deux cas :

  • default-src 'none' : par défaut, aucun contenu ne peut être chargé ; on travaille en mode « liste blanche » ;
  • img-src 'self'; font-src 'self' : les images et fontes peuvent être chargées depuis la même origine (https://nos-oignons.net) ;
  • form-action 'self' : les formulaires ne peuvent pas déclancher de requête vers un domaine tiers ;
  • frame-ancestors 'none' : notre site ne peut pas être embarqué dans une « iframe » ;
  • upgrade-insecure-requests; block-all-mixed-content : les requêtes HTTP sans TLS sont interdites, et remplacées par HTTPS automatiquement ;
  • sandbox allow-forms allow-same-origin allow-scripts : le bac à sable est configuré pour interdire les popups, les greffons du navigateur, l'API Javascript de navigation, et cette permettant de manipuler la souris.
  • reflected-xss block : on demande au navigateur d'utiliser des heuristiques qui détectent les tentatives de reflected XSS, cf X-Xss-Protection ;
  • referrer origin-when-cross-origin : lorsque le domaine change, le navigateur ne doit pas fournir plus d'informations que « le site dont on vient est nos-oignons.net. »

Politique par défaut

On définit les paramètres suivants :

  • script-src 'self'; style-src 'self' : on peut charger des scripts et des feuilles de styles depuis le domaine courant ;
  • connect-src https://onionoo.torproject.org/ : les scripts peuvent se connecter à Onionoo (requis par Graphnion).

Politique « inline »

On définit, à la place, ces paramêtres :

  • script-src 'self' 'unsafe-inline' 'unsafe-eval' : on autorise le Javascript inline et l'utilisation de eval() (requise par RT);
  • style-src 'self' 'unsafe-inline' : on autorise le CSS inline.
  • connect-src 'self' : requis par Request Tracker.

Implémentation dans Nginx

La politique est implémentée dans Nginx de cette façon :

  • le fichier snippets/csp-header.conf est inclus dans le bloc server, et définit la politique par défaut :

      # En-tête Content-Security-Policy pour la majeure partie du site.
      add_header "Content-Security-Policy" "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src https://onionoo.torproject.org/; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; sandbox allow-forms allow-same-origin allow-scripts; reflected-xss block; referrer origin-when-cross-origin;";
    
  • le fichier snippets/csp-inline.conf est inclus dans chaque bloc location qui doit utiliser la politique plus laxe :

      # En-tête Content-Security-Policy plus laxe pour AWStats, Request Tracker et gitweb
      add_header "Content-Security-Policy" "default-src 'none'; connect-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'
      ; img-src 'self'; font-src 'self'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; sandbox allow-forms allow-sa
      me-origin allow-scripts; reflected-xss block; referrer origin-when-cross-origin;";
    
      # On (ré-)inclus les en-têtes définis au par avant,
      # parce que utiliser add_header invalide tous les en-têtes définis dans un contexte moins spécifique
      include snippets/headers.conf;
    

En-têtes complémentaires

Quelques en-têtes complémentaires sont définis dans snippets/headers.conf, qui est inclus au début du bloc server :

    # Ne laisse pas charger notre site dans une <frame>.
    # Ça empêche les attaques par « clickjacking »
    add_header "X-Frame-Options" "deny";

    # Active une protection contre le XSS réflectif dans IE, Chrome & Safari
    # Il faudra qu'on vérifie que ça ne casse pas de scripts chez nous.
    add_header "X-Xss-Protection" "1; mode=block";

    # Force le navigateur à honorer le type MIME transmi par le serveur.
    # Ça peut éviter qu'une page HTML ou une image soit subitement
    # interpretée comme un JavaScript ou comme un .exe
    add_header "X-Content-Type-Options" "nosniff";

Statistiques

Les statistiques sont accessibles sur https://nos-oignons.net/stats/. Les pages web (statiques) sont régénérées chaque nuit. L'accès est protégé avec la même base d'authentification que celle du wiki du conseil d'administration.

Les requêtes vers les services internes sont volontairement ignorées des stats : wiki du C.A., wiki admin. sys., interface de modification du site web, Request Tracker et accès aux statistiques elles-mêmes.

Les fichiers utilisés :

/etc/awstats/awstats.conf
Configuration par défaut livrée avec le paquet Debian. Ne pas toucher.
/etc/awstats/awstats.conf.local
Configuration locale pour tous les sites.
/etc/awstats/awstats.nos-oignons.net.conf
Configuration pour le site de Nos oignons.
/etc/cron.d/awstats
Configuration des processus réguliers (mise à jour des données et des pages statiques).
/etc/logrotate.d/nginx
Déclenchement d'une mise à jour des statistiques dans une directive prerotate, et application d'une ACL sur les nouveaux fichiers .log dans une directive postrotate.
/etc/logrotate.d/httpd-prerotate/awstats
Mise à jour des pages statiques juste avant la rotation des fichiers journaux du serveur Web.
/etc/default/awstats
Réglages pour les scripts de mise à jour.
/etc/sudoers.d/04_awstats
Ajoute une exception à l'option Defaults requiretty pour que le script /usr/share/awstats/tools/update.sh puisse être exécuté dans logrotate.
/srv/awstats/data
Répertoire contenant les données extraites des logs.
/var/cache/awstats
Répertoire contenant des données temporaires permettant d'accélerer les calculs. Typiquement des résolutions DNS.
/srv/awstats/html
Contient des liens vers les répertoires avec les statiques générées dans /var/cache/awstats.
/var/log/nginx/access.log
Fichier de journalisation des accès au service web utilisé pour produire les statistiques.

Afin de ne pas laisser au même utilisateur www-data le service des pages de statistiques et leur production, on a créé un utilisateur dédié, awstats :

sudo adduser --system --gecos "Web statistics" --home /srv/awstats --shell /usr/sbin/nologin --group awstats

puis remplacé www-data par awstats dans la crontab, et rendu à awstats:awstats la propriété du contenu des répertoires /srv/awstats/data et /var/cache/awstats/. Ces répertoires eux-mêmes continuent d'appartenir au groupe www-data.

Pour permettre à l'utilisateur awstats d'accéder aux logs de Nginx, une ACL est appliquée sur le fichier concerné depuis la directive postrotate du fichier /etc/logrotate.d/nginx :

setfacl -m g:awstats:r /var/log/nginx/access.log

Gitweb

Afin d'avoir une interface web pour naviguer dans les dépôts Git, gitweb est installé et accessible à l'adresse : https://nos-oignons.net/gitweb/

Sa configuration se trouve dans /etc/gitweb.conf et dans /etc/nginx/sites-available/https. Les dépôts listés sont ceux présents dans le répertoire /var/cache/git.

Interaction avec les scripts CGI

Nginx ne sachant pas interagir avec les scripts CGI (et leur préfère FastCGI), on utilise fcgiwrap.

ATTENTION : fcgiwrap permet (à quiconque peut écrire sur le socket FastCGI) de lancer des commandes arbitraires. Pour atténuer le problème, on a des instances de fcgiwrap séparées pour un nombre restreint d'utilisateurs (list, git, wiki-admin, wiki-ca, website), et confinées à l'aide des fonctionnalités de sécurité de systemd.

Pour un utilisateur donné, l'instance de fcgiwrap écoute sur le socket /run/fcgiwrap-${USERNAME}.sock (en fait, systemd écoute là, puisqu'on utilise la socket activation). On peut obtenir les logs associés à une instance de fcgiwrap donnée en invoquant sudo journalctl -u fcgiwrap@${USERNAME}.

Modèle de service

Pour nous éviter de longs copier-coller, on utilise un modèle d'unit file systemd :

# /etc/systemd/system/fcgiwrap@.service
[Unit]
Description=Simple CGI Server as user %i
After=nss-user-lookup.target

[Service]
ExecStart=/usr/sbin/fcgiwrap
User=%i
Group=%i

PrivateTmp=true
PrivateDevices=true
PrivateNetwork=true
ProtectSystem=full
ProtectHome=true

NoNewPrivileges=true
CapabilityBoundingSet=

InaccessibleDirectories=/srv/association /srv/awstats /srv/postgresql
InaccessibleDirectories=/srv/request-tracker4 /srv/schleuder


[Install]
Also=fcgiwrap@%i.socket

# /etc/systemd/system/fcgiwrap@.socket
[Unit]
Description=fcgiwrap socket for user %i

[Socket]
ListenStream=/run/fcgiwrap-%i.sock
SocketUser=www-data
SocketGroup=www-data
SocketMode=0600

[Install]
WantedBy=sockets.target

On peut dès lors ajouter une instance de fcgiwrap très simplement :

systemctl enable fcgiwrap@user2
systemctl start  fcgiwrap@user2

Confinement spécifique

Les unit files pour fcgiwrap contiennent déjà des mesures de sécurité génériques. Cependant, on peut vouloir appliquer des restrictions supplémentaires à une instance donnée.

Ça se fait très simplement en utilisant un drop in : un fichier qui s'ajoute à la configuration d'un service.

# /etc/systemd/system/fcgiwrap@git.service.d/sandbox.conf
[Service]
InaccessibleDirectories=/var/log

InaccessibleDirectories=/srv/git /srv/mailman /srv/ikiwiki
InaccessibleDirectories=/srv/http/awstats /srv/http/campagne2015
InaccessibleDirectories=/srv/http/website /srv/http/website-ecrire
InaccessibleDirectories=/srv/http/wiki-admin /srv/http/wiki-admin-ecrire
InaccessibleDirectories=/srv/http/wiki-ca