Installation et configuration¶
Ce chapitre couvre le déploiement pas-a-pas de la plateforme documentaire : installation de MkDocs Material, creation du plugin classification-filter, configuration du pipeline CI multi-build, mise en place du reverse proxy Caddy et integration Keycloak pour le contrôle d'accès par classification.
Pre-requis¶
| Composant | Version minimum | Vérification |
|---|---|---|
| Python | 3.10+ | python --version |
| pip | 23+ | pip --version |
| Git | 2.40+ | git --version |
| Caddy | 2.7+ | caddy version |
| Keycloak | 22+ (operationnel) | curl https://iam.company.io/health |
| Gitea Actions | Gitea 1.21+ ou équivalent CI | Voir documentation CI |
Installation MkDocs Material¶
Isoler les dependances dans un environnement virtuel est recommande pour eviter les conflits : python -m venv .venv && source .venv/bin/activate, puis reinstaller les paquets ci-dessus.
Plugin classification-filter¶
Le plugin lit la metadonnee classification dans le front matter de chaque page et exclut les pages dont le niveau depasse la cible du build (variable MKDOCS_CLASSIFICATION).
Structure du paquet¶
mkdocs-classification-filter/
├── setup.py
└── mkdocs_classification_filter/
├── __init__.py
└── plugin.py
setup.py¶
from setuptools import setup, find_packages
setup(
name="mkdocs-classification-filter",
version="0.1.0",
packages=find_packages(),
entry_points={
"mkdocs.plugins": [
"classification-filter = mkdocs_classification_filter.plugin:ClassificationFilterPlugin",
]
},
install_requires=["mkdocs>=1.5"],
)
plugin.py¶
import os
import logging
from mkdocs.plugins import BasePlugin
log = logging.getLogger("mkdocs.plugins.classification-filter")
LEVELS = {"public": 0, "interne": 1, "confidentiel": 2, "restreint": 3}
class ClassificationFilterPlugin(BasePlugin):
def on_files(self, files, config):
target = os.environ.get("MKDOCS_CLASSIFICATION", "public").lower()
if target not in LEVELS:
raise ValueError(
f"MKDOCS_CLASSIFICATION='{target}' invalide. "
f"Valeurs autorisees : {', '.join(LEVELS.keys())}"
)
target_level = LEVELS[target]
log.info(f"Classification filter: build niveau '{target}' (<= {target_level})")
to_remove = []
for file in files:
if not file.src_path.endswith(".md"):
continue
page_level = self._get_level(file, config)
if page_level is not None and page_level > target_level:
log.info(f" Exclusion: {file.src_path}")
to_remove.append(file)
for file in to_remove:
files.remove(file)
return files
def on_nav(self, nav, config, files):
self._clean_nav(nav.items)
return nav
def on_env(self, env, config, files):
target = os.environ.get("MKDOCS_CLASSIFICATION", "public").lower()
env.globals["classification_level"] = target
return env
def _get_level(self, file, config):
import yaml
full_path = os.path.join(config["docs_dir"], file.src_path)
try:
with open(full_path, "r", encoding="utf-8") as f:
content = f.read()
except (OSError, IOError):
return None
if not content.startswith("---"):
return LEVELS["public"]
parts = content.split("---", 2)
if len(parts) < 3:
return LEVELS["public"]
try:
meta = yaml.safe_load(parts[1])
except yaml.YAMLError:
return LEVELS["public"]
if not meta or "classification" not in meta:
return LEVELS["public"]
classification = str(meta["classification"]).lower()
if classification not in LEVELS:
raise ValueError(
f"classification '{classification}' invalide dans {file.src_path}. "
f"Valeurs autorisees : {', '.join(LEVELS.keys())}"
)
return LEVELS[classification]
def _clean_nav(self, items):
to_remove = []
for item in items:
if hasattr(item, "children") and item.children:
self._clean_nav(item.children)
if not item.children:
to_remove.append(item)
elif hasattr(item, "file") and item.file is None:
to_remove.append(item)
for item in to_remove:
items.remove(item)
Installation locale¶
Configuration mkdocs.yml¶
Ajouter classification-filter avant minify dans la section plugins :
plugins:
- search:
lang: fr
- classification-filter
- blog:
blog_dir: blog
- minify:
minify_html: true
Chaque page qui doit etre filtree declare sa classification dans son front matter :
Les pages sans champ classification sont traitees comme public par defaut.
Test local¶
# Build public
MKDOCS_CLASSIFICATION=public mkdocs build --site-dir site-public
# Build interne
MKDOCS_CLASSIFICATION=interne mkdocs build --site-dir site-interne
# Build confidentiel
MKDOCS_CLASSIFICATION=confidentiel mkdocs build --site-dir site-confidentiel
# Build restreint (toutes les pages)
MKDOCS_CLASSIFICATION=restreint mkdocs build --site-dir site-restreint
Variable non definie
Si MKDOCS_CLASSIFICATION n'est pas definie, le plugin utilise public par defaut. Le pipeline CI doit toujours positionner la variable explicitement.
Pipeline CI (Gitea Actions)¶
Le pipeline construit les quatre artefacts en parallele via une matrice, puis les deploie dans les sous-répertoires correspondants de la racine web.
# .gitea/workflows/docs.yml
name: docs-multi-build
on:
push:
branches:
- main
jobs:
build:
name: Build ${{ matrix.level }}
runs-on: ubuntu-latest
strategy:
matrix:
level: [public, interne, confidentiel, restreint]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
pip install mkdocs-material mkdocs-minify-plugin
pip install -e mkdocs-classification-filter/
- name: Build
env:
MKDOCS_CLASSIFICATION: ${{ matrix.level }}
run: mkdocs build --site-dir site-${{ matrix.level }}
- uses: actions/upload-artifact@v4
with:
name: docs-${{ matrix.level }}
path: site-${{ matrix.level }}/
retention-days: 7
deploy:
name: Deploy
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/download-artifact@v4
with:
path: artifacts/
- name: Deploy via rsync
env:
SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
run: |
echo "$SSH_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
for level in public interne confidentiel restreint; do
rsync -avz --delete \
-e "ssh -i /tmp/deploy_key -o StrictHostKeyChecking=no" \
artifacts/docs-${level}/ \
${DEPLOY_USER}@${DEPLOY_HOST}:/var/www/docs/${level}/
done
rm -f /tmp/deploy_key
Reverse proxy Caddy¶
Caddy route chaque requête vers le build correspondant au niveau de classification transmis par oauth2-proxy dans le header X-Doc-Classification.
# /etc/caddy/Caddyfile
{
email admin@company.io
}
docs.company.io {
header {
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
Referrer-Policy strict-origin-when-cross-origin
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
-Server
}
forward_auth oauth2-proxy:4180 {
uri /oauth2/auth
copy_headers X-Forwarded-User X-Doc-Classification
}
@restreint { header X-Doc-Classification restreint }
@confidentiel { header X-Doc-Classification confidentiel }
@interne { header X-Doc-Classification interne }
handle @restreint { root * /var/www/docs/restreint; file_server }
handle @confidentiel { root * /var/www/docs/confidentiel; file_server }
handle @interne { root * /var/www/docs/interne; file_server }
handle { root * /var/www/docs/public; file_server }
log {
output file /var/log/caddy/docs-access.log
format json
}
}
oauth2-proxy
oauth2-proxy valide le token OIDC aupres de Keycloak et enrichit la requête avec le claim doc_classification converti en header X-Doc-Classification.
Configuration Keycloak¶
Créer le client OIDC¶
Dans la console Keycloak, realm company, créer un client docs-platform :
- Client type : OpenID Connect
- Client authentication : On (flux confidentiel)
- Valid redirect URIs :
https://docs.company.io/oauth2/callback
Protocol mapper pour doc_classification¶
Dans Clients > docs-platform > Client scopes > Add mapper > By configuration, choisir User Attribute :
| Champ | Valeur |
|---|---|
| Name | doc-classification |
| User attribute | doc_classification |
| Token Claim Name | doc_classification |
| Claim JSON Type | String |
| Add to access token | On |
| Add to userinfo | On |
Groupes de classification¶
Créer trois groupes dans Groups et positionner l'attribut doc_classification via Attributes :
| Groupe | Attribut doc_classification |
|---|---|
| doc-interne | interne |
| doc-confidentiel | confidentiel |
| doc-restreint | restreint |
Les utilisateurs sans groupe obtiennent la valeur par defaut public (fallback Caddy).
Test de bout en bout¶
# Verifier que les quatre builds sont deployes
for level in public interne confidentiel restreint; do
echo -n "Build $level : "
curl -sf "https://docs.company.io/index.html" \
-H "X-Doc-Classification: $level" -o /dev/null && echo "OK" || echo "KO"
done
# Verifier qu'une page confidentielle est absente du build public
curl -sf "https://docs.company.io/procedure-confidentielle/" \
-H "X-Doc-Classification: public" \
-o /dev/null && echo "KO: page visible" || echo "OK: page absente"
# Verifier le claim OIDC
curl -s "https://docs.company.io/oauth2/userinfo" \
-H "Cookie: _oauth2_proxy=<token>" | jq '.doc_classification'
| Vérification | Attendu |
|---|---|
mkdocs --version | Version affichee |
| Build sans erreur d'import | Plugin charge |
| Page interne absente du build public | Fichier absent |
| HTTPS docs.company.io | HTTP/2 200 |
| Accès sans cookie | Redirect vers Keycloak |
Claim doc_classification present | Valeur conforme au groupe |
Synthese¶
La plateforme est operationnelle apres quatre étapes : installation de MkDocs Material et du plugin classification-filter, configuration du pipeline CI qui produit quatre artefacts distincts, mise en place du reverse proxy Caddy qui route selon le claim OIDC, et parametrage Keycloak qui attribue le niveau correct à chaque utilisateur. Le chapitre suivant couvre l'integration avec les outils de l'entreprise (SSO, monitoring, gestion des accès en self-service).