Exposition et protection de GetHomepage dans Kubernetes
En multipliant les services exposés sur la toile, il est primordial de les protéger à minima. Déployant mes services dans k3s (article disponible ici ), je vous propose de mettre l’outil Anubis dans la boucle.
Récemment, j’ai écrit cet article concernant Anubis et sa mise en place dans une machine virtuelle :
Anubis, l’outil qui bloque la récolte agressiveLe web est un vaste terrain de jeu. Que ce soit des humains ou des robots, il y a une quantité de données et de flux qui se croisent et se bousculent. Ces dernières années, ce sont plutôt les robots de récolte de données qui prennent de plus en plus
Étant en pleine migration de mes services d’un VPS à l’autre (quelques infos dans cette page ), je me suis mis en quête de conteneuriser le maximum de service possible. Ainsi, dès qu’un outil sera déployé, il aura son instance Anubis à côté. Pour cet exemple, je vais déployer GetHomepage et Anubis pour qu’ils fonctionnent ensemble.
Rappel du contexte et prérequis#
Avant de se lancer dans le déploiement des services, il est nécessaire de se préparer un minimum. Vous devez avoir les droits et accès au cluster Kubernetes, et quelques notions quant à l’écosystème Kubernetes.
Dans mon cas, Kubernetes est exécuté via K3S sur un VPS, avec Cilium comme CNI. Étant donné que je n’ai qu’une seule machine servant de control-plane et indirectement de worker, je n’utilise ni ingress ni gateway-api, mais j’expose les services via NodePort. Le stockage n’est pas nécessaire ici, donc je n’aborderai pas la CSI. Traefik se trouve devant K3S pour rediriger le trafic externe vers les services exposés via NodePort. Enfin, l’accès à la machine nécessite de passer par Cloudflare.
💡 J’aimerais utiliser un service souverain, mais (malheureusement) Cloudflare est le seul prestataire offrant un large éventail de service gratuitement, sans (trop de) limites.
Web -> Cloudflare -> OVHcloud -> [VPS = Traefik -> NodePort -> K3S (Cilium) -> Services -> Pod]Configuration de Cloudflare#
Dans le panneau d’administration Cloudflare, j’ai créé des enregistrements DNS de type A pour chaque domaine et sous-domaine concerné. La case “Proxy” est cochée, forçant le flux à transiter via Cloudflare avant d’arriver à sa destination.
Côté SSL/TLS, la configuration a son importance. Après plusieurs essais, le plus pertinent est d’être dans le mode encryption "SSL/TLS full (strict)". La documentation est claire et vous fournira de nombreux détails, sur le site officiel ici :
Encryption modesEncryption modes allow you to control how Cloudflare connects to your origin web server and how certificates presented by your origin are validated.Cloudflare DocsAvec le mode de chiffrement “full (strict)”, le flux entre Cloudflare et le VPS est chiffré, et est certifié avec la PKI de Cloudflare. Il est nécessaire d’utiliser les certificats Let’s Encrypt générés par Cloudflare pour que tout le système de chiffrement fonctionne de bout en bout. La génération des certificats s’effectuera par le biais de Traefik, automagiquement. Les certificats concernent uniquement la bordure, ce qui est visible et est exposé sur le web. Il ne s’agit pas de générer des certificats dans Kubernetes (pas de mTLS donc).
Configuration de Traefik#
C’est finalement Traefik qui m’a donné le plus de tissu à broder. Entre les headers qui sont différents d’une source à l’autre à cause / grâce à Cloudflare, la source n’est jamais la même. Quelques configurations s’imposent pour que tout soit en ordre. Dans cet environnement, seuls les IP provenant des sous-réseaux de Cloudflare seront reconnus et acceptés. Pour tous les autres sous-réseaux et IP, une erreur SSL et le code HTTP 523 s’affichera.
Trafik est installé en dur sur mon VPS (documentation sur J.HOMMET.NET ici
), dans le dossier /etc/traefik. Les configurations dynamiques seront dans le dossier /etc/traefik/dynamic, et les certificats seront stockés dans le fichier /etc/traefik.acme.json. Créez, changez ou mappez ces dossiers et fichiers selon votre environnement.
Pour Traefik, la logique sera la suivante :
Entrée unique en TCP/443 -> vérification des headers -> si validé, redirection vers le routeur traefik -> redirection vers le service anubisPassons aux fichiers de configuration. Voici le fichier de configuration statique traefik.yml :
---
global:
sendAnonymousUsage: true
checkNewVersion: false
api:
dashboard: false
debug: false
providers:
file:
directory: "/etc/traefik/dynamic"
watch: true
entryPoints:
websecure:
address: ":443"
forwardedHeaders:
trustedIPs:
# Plages IP Cloudflare (à jour août 2025)
- "173.245.48.0/20"
- "103.21.244.0/22"
- "103.22.200.0/22"
- "103.31.4.0/22"
- "141.101.64.0/18"
- "108.162.192.0/18"
- "190.93.240.0/20"
- "188.114.96.0/20"
- "197.234.240.0/22"
- "198.41.128.0/17"
- "162.158.0.0/15"
- "104.16.0.0/13"
- "104.24.0.0/14"
- "172.64.0.0/13"
- "131.0.72.0/22"
- "2400:cb00::/32"
- "2606:4700::/32"
- "2803:f800::/32"
- "2405:b500::/32"
- "2405:8100::/32"
- "2a06:98c0::/29"
- "2c0f:f248::/32"
certificatesResolvers:
cloudflare:
acme:
email: email@mail.net
# caServer: https://acme-staging-v02.api.letsencrypt.org/directory
caServer: https://acme-v02.api.letsencrypt.org/directory
storage: /etc/traefik/acme.json
certificatesDuration: 2160 # 90 jours
keyType: EC256
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"Et le fichier de configuration dynamique pour exposer le service Anubis pour GetHomepage (fichier rt-k3s-gethomepage.yml):
http:
serversTransports:
cftransport:
insecureSkipVerify: true
middlewares:
headersLight:
headers:
frameDeny: true
browserXssFilter: true
hostsProxyHeaders:
- "X-Forwarded-Host"
- "X-Forwarded-Proto"
- "X-Forwarded-For"
services:
svc-k3s-gethomepage:
loadBalancer:
serversTransport: cftransport
servers:
- url: "http://localhost:31000"
routers:
rt-homepage:
entryPoints:
- "websecure"
rule: "Host(`hommet.net`)"
service: "svc-k3s-gethomepage"
middlewares:
- headersLight
tls:
certResolver: cloudflareLa configuration Traefik donnée sert principalement à router le trafic HTTPS sécurisé vers un service local avec gestion de certificats via Cloudflare, en appliquant des règles de sécurité sur les headers HTTP.
- Elle définit un routeur HTTP sécurisé (HTTPS) sur l’entrée “websecure” qui redirige les requêtes ayant pour hôte “hommet.net” vers le service exposé par NodePort dans K3S sur le port local 31000 (http://localhost:31000 ).
- Le service
svc-k3s-gethomepageest un load balancer qui pointe vers ce serveur local. - Un middleware
headersLightapplique des en-têtes HTTP pour renforcer la sécurité du navigateur, notamment en bloquant l’affichage dans des frames (frameDeny) et activant un filtre XSS côté navigateur. Trois en-têtes HTTP sont envoyés pour transmettre des informations sur l’origine de la requête au backend. Cela permet de récupérer l’adresse IP d’origine, de vérifier qu’elle provient de Cloudflare, et de s’assurer qu’elle correspond à un sous-réseau de la liste des « trustedIPs » dans la configuration statique. - Le TLS est activé via un
certResolverconfiguré pour utiliser Cloudflare pour la gestion automatique des certificats SSL publics. - Le middleware
serverTransports: insecureSkipVerify: trueest important et nécessaire, pour désactiver la vérification TLS du certificat du serveur backend par Traefik quand il communique avec le service local. Ceci est utile ici parce que le backend local peut avoir un certificat autosigné ou non valide pour HTTPS (ne correspondant pas au SNI du certificat de Cloudflare), et Traefik ne bloque pas la connexion TLS vers ce backend. En utilisant Cloudflare comme résolveur de certificat public pour l’entrée HTTPS, Traefik gère des certificats publics valides pour les clients externes, mais ignore la validation interne pour le backend. Cela évite des erreurs TLS sans compromettre la sécurité client externe.
L’idéal serait un chiffrement réellement de bout en bout (du web jusqu’au pod).
Configuration des services Anubis et GetHomepage#
Maintenant, il est temps de déployer GetHomepage dans Kubernetes, avec Anubis à ses côtés. Voici le manifest complet pour les deux services, avec tout le nécessaire (namespace, serviceAccount, configMaps…). De plus, quelques sécurités sont configurées pour avoir le meilleur déploiement possible, sans trop de blocage (serviceAccount, UID et GID à 1000, pas de root…) :
apiVersion: v1
kind: Namespace
metadata:
name: gethomepage
apiVersion: v1
---
kind: ServiceAccount
metadata:
name: homepage
namespace: gethomepage
labels:
app.kubernetes.io/name: homepage
secrets:
- name: homepage
---
apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
name: homepage
namespace: gethomepage
labels:
app.kubernetes.io/name: homepage
annotations:
kubernetes.io/service-account.name: homepage
---
apiVersion: v1
kind: ConfigMap
metadata:
name: anubis-policy
namespace: gethomepage
data:
anubis-policy.json: |
{
"bots": [
{
"name": "internal-api-traffic",
"remote_addresses": ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"],
"action": "ALLOW"
},
{
"name": "kubernetes-api",
"path_regex": "^/api/.*$",
"action": "ALLOW"
},
{
"name": "kubernetes-apis",
"path_regex": "^/apis/.*$",
"action": "ALLOW"
},
{
"name": "homepage-backend-api",
"user_agent_regex": "(?i:homepage|gethomepage)",
"action": "ALLOW"
},
{
"name": "cloudflare-insights",
"path_regex": "^/beacon\\.min\\.js$",
"action": "ALLOW"
},
{
"name": "static-files",
"path_regex": "^/(favicon\\.ico|robots\\.txt|sitemap\\.xml|.*\\.(css|js|png|jpg|jpeg|gif|webp|ico|svg))$",
"action": "ALLOW"
},
{
"name": "anubis-api",
"path_regex": "^/\\.within\\.website/x/cmd/anubis/api/.*$",
"action": "ALLOW"
},
{
"name": "regular-browsers",
"user_agent_regex": "Mozilla|Chrome|Safari|Firefox|Edge",
"action": "CHALLENGE"
}
]
}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: homepage
namespace: gethomepage
labels:
app.kubernetes.io/name: homepage
data:
kubernetes.yaml: |
mode: cluster
settings.yaml: |
title: HOMMET.NET index
description: Un monde à part.
language: fr
custom.css: ""
custom.js: ""
bookmarks.yaml: |
- HOMMET.NET:
- Blog:
- abbr: JH
href: https://j.hommet.net
services.yaml: ""
widgets.yaml: |
- kubernetes:
cluster:
show: false
cpu: true
memory: true
showLabel: true
label: "VPS K3S"
nodes:
show: true
cpu: true
memory: true
showLabel: false
- resources:
backend: resources
expanded: true
cpu: true
memory: true
network: default
- search:
provider: duckduckgo
target: _blank
proxmox.yaml: ""
docker.yaml: ""
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: homepage
labels:
app.kubernetes.io/name: homepage
rules:
- apiGroups:
- ""
resources:
- namespaces
- pods
- nodes
verbs:
- get
- list
- apiGroups:
- extensions
- networking.k8s.io
resources:
- ingresses
verbs:
- get
- list
- apiGroups:
- traefik.io
resources:
- ingressroutes
verbs:
- get
- list
- apiGroups:
- gateway.networking.k8s.io
resources:
- httproutes
- gateways
verbs:
- get
- list
- apiGroups:
- metrics.k8s.io
resources:
- nodes
- pods
verbs:
- get
- list
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: homepage
labels:
app.kubernetes.io/name: homepage
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: homepage
subjects:
- kind: ServiceAccount
name: homepage
namespace: gethomepage
---
apiVersion: v1
kind: Service
metadata:
name: homepage
namespace: gethomepage
labels:
app.kubernetes.io/name: homepage
annotations:
spec:
type: NodePort
ports:
- protocol: TCP
port: 8080
targetPort: 8080
name: anubis
nodePort: 31000
selector:
app.kubernetes.io/name: homepage
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: homepage
namespace: gethomepage
labels:
app.kubernetes.io/name: homepage
spec:
revisionHistoryLimit: 3
replicas: 1
strategy:
type: RollingUpdate
selector:
matchLabels:
app.kubernetes.io/name: homepage
template:
metadata:
labels:
app.kubernetes.io/name: homepage
spec:
serviceAccountName: homepage
automountServiceAccountToken: true
dnsPolicy: ClusterFirst
enableServiceLinks: true
securityContext:
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: homepage
image: "ghcr.io/gethomepage/homepage:v1.4"
imagePullPolicy: IfNotPresent
env:
- name: HOMEPAGE_ALLOWED_HOSTS
value: hommet.net # required, may need port. See gethomepage.dev/installation/#homepage_allowed_hosts
ports:
- name: http
containerPort: 3000
protocol: TCP
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 250m
memory: 64Mi
securityContext:
allowPrivilegeEscalation: false
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true
capabilities:
drop:
- ALL
volumeMounts:
- mountPath: /app/config/custom.js
name: homepage-config
subPath: custom.js
- mountPath: /app/config/custom.css
name: homepage-config
subPath: custom.css
- mountPath: /app/config/bookmarks.yaml
name: homepage-config
subPath: bookmarks.yaml
- mountPath: /app/config/docker.yaml
name: homepage-config
subPath: docker.yaml
- mountPath: /app/config/kubernetes.yaml
name: homepage-config
subPath: kubernetes.yaml
- mountPath: /app/config/proxmox.yaml
name: homepage-config
subPath: proxmox.yaml
- mountPath: /app/config/services.yaml
name: homepage-config
subPath: services.yaml
- mountPath: /app/config/settings.yaml
name: homepage-config
subPath: settings.yaml
- mountPath: /app/config/widgets.yaml
name: homepage-config
subPath: widgets.yaml
- mountPath: /app/config/logs
name: logs
- name: anubis
image: ghcr.io/techarohq/anubis:latest
imagePullPolicy: Always
env:
- name: BIND
value: ":8080"
- name: DIFFICULTY
value: "4"
- name: SERVE_ROBOTS_TXT
value: "false"
- name: OG_PASSTHROUGH
value: "true" # Sert à passer les requêtes OG si déjà validé
- name: OG_EXPIRY_TIME
value: "48h" # Durée cache OG allongée pour moins de tests
- name: OG_MAX_QUEUE_LENGTH
value: "50" # Limiter taille file d’attente OG (prévenir montée en charge)
- name: TIMEOUT_HTTP
value: "15s"
- name: REDIRECT_DOMAINS
value: "hommet.net"
- name: TARGET
value: "http://localhost:3000"
- name: COOKIE_DOMAIN
value: "hommet.net"
- name: POLICY_FNAME
value: "/app/config/anubis-policy.json"
- name: REDIRECT_URL
value: "https://hommet.net/"
- name: TRUSTED_PROXIES
value: "173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32"
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 250m
memory: 64Mi
securityContext:
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
volumeMounts:
- mountPath: /app/config/anubis-policy.json
name: anubis-policy
subPath: anubis-policy.json
readinessProbe:
exec:
command: ["anubis", "--healthcheck"]
initialDelaySeconds: 1
periodSeconds: 5
timeoutSeconds: 30
failureThreshold: 5
livenessProbe:
exec:
command: ["anubis", "--healthcheck"]
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 30
failureThreshold: 5
volumes:
- name: anubis-policy
configMap:
name: anubis-policy
- name: homepage-config
configMap:
name: homepage
- name: logs
emptyDir: {}La configuration et le déploiement d’Anubis est assez simple, que ce soit pour Kubernetes qu’en dur sur une machine. La première étape consiste à créer le namespace :
kubectl create ns gethomepageDans le déploiement de Gethomepage, il faut laisser les montages de tous les fichiers de configuration même si vous ne vous en servez pas. Ici, je n’utilise pas docker.yaml ni proxmox.yaml, mais Gethomepage ne démarrera pas s’il ne trouve pas ces fichiers (même vide).
Enregistrez ce manifest dans un fichier (par exemple gethomepage.yaml) et déployez-le kubectl apply -f gethomepage.yaml.
Lors du premier déploiement, je n’arrivais plus à avoir les informations des widgets de Gethomepage. En effet, la page Gethomepage se rafraîchit toutes les quelques secondes pour mettre à jour les widgets. Avec Anubis devant, ça obligeait à refaire le challenge de navigateur pour rafraichir la page et l’afficher ; ce n’était pas viable et ça finissait par saturer la machine. Ainsi, dans la configuration d’Anubis, quelques règles sont en “ALLOW” pour esquiver le challenge et éviter de bloquer les ressources internes/locales à Gethomepage et à Cloudflare.
Vérifiez que tous les deux pods sont lancés, que le statut est bien Ready et que le service fonctionne, en accédant à l’URL publique de GetHomepage (pour ma part, https://hommet.net
).
kubectl get svc,pod,deploy,cm,sa -n gethomepage
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/homepage NodePort 10.43.103.126 <none> 3000:32661/TCP,8080:31000/TCP 16h
NAME READY STATUS RESTARTS AGE
pod/homepage-6b654b7f9c-fjrsv 2/2 Running 0 14h
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/homepage 1/1 1 1 16h
NAME DATA AGE
configmap/anubis-policy 1 14h
configmap/homepage 9 15h
configmap/kube-root-ca.crt 1 16h
NAME SECRETS AGE
serviceaccount/homepage 1 16hSources#
Kubernetes Installation - HomepageInstall on KuberneteslogoKubernetes | AnubisWhen setting up Anubis in Kubernetes, you want to make sure that you thread requests through Anubis kinda like this:Help with insecureSkipVerifyEnable Traefik access log in JSON format (doc) and share the output from a request.Traefik Labs Community Forumbluepuma77Plages IPCette page constitue la source définitive des plages IP actuelles de Cloudflare.Logo Cloudflare, couleur
- Mots-clés
- #cloudflare #traefik #kubernetes
- Auteur
- Julien HOMMET
- date +"%Y-%m-%d"
- Temps_lecture
- 11 minutes
- quantité_mots
- 2257 mots
- Catégorie
- tuto
- maj $(date +"%Y-%m-%d")