Tutti i post

Reverse Proxy su K3s con Traefik: Endpoint per Servizi Esterni al Cluster

Come configurare un mini cluster K3s come reverse proxy per servizi che girano su container esterni, usando Traefik, EndpointSlice e middleware di sicurezza.

Il Contesto

Ho un mini cluster K3s composto da due VM Debian su Proxmox. Le VM sono state sottoposte a hardening di base: accesso SSH solo con chiave, servizi non necessari disabilitati, firewall nftables con policy drop di default. Il cluster si trova dietro una LAN privata, raggiungibile dall’esterno tramite port forwarding configurato sul router.

Il problema concreto: sulla stessa rete locale girano diversi servizi containerizzati su host separati dal cluster. Questi servizi hanno bisogno di TLS, di un dominio pubblico e di un minimo di protezione a livello di header e rate limiting. Installarli uno per uno su ogni host non ha senso. Serve un punto di ingresso unico.

La soluzione adottata consiste nell’usare il cluster K3s come reverse proxy centralizzato. Traefik, l’ingress controller integrato in K3s, gestisce la terminazione TLS e l’applicazione dei middleware di sicurezza. I servizi esterni vengono esposti tramite Service headless e EndpointSlice che puntano all’IP LAN del container.


Quando Ha Senso Usare K3s Come Reverse Proxy

Esistono scenari in cui questo approccio risulta ragionevole, e altri in cui aggiunge complessita senza un ritorno reale.

Ha senso quando:

  • Sulla rete locale esistono piu servizi che richiedono TLS e un dominio pubblico. Gestire certificati e rinnovi su ogni singolo host diventa rapidamente un problema di manutenzione.
  • Si vuole applicare una policy di sicurezza uniforme (HSTS, CSP, rate limiting) senza replicare la configurazione su ogni servizio.
  • Si ha già un cluster K3s attivo per altri scopi. Aggiungere un ingress per un servizio esterno costa poche righe di YAML.
  • Il servizio esterno non ha un reverse proxy proprio o ne ha uno limitato.

Non ha senso quando:

  • Si ha un singolo servizio da esporre. Un reverse proxy dedicato come Caddy o un semplice Nginx bastano e sono piu semplici da mantenere.
  • Il servizio è già dietro un proprio reverse proxy con TLS. Aggiungere un secondo layer di proxy introduce latenza e complessita di debug senza benefici evidenti.
  • Non si ha familiarità con Kubernetes. Il costo di apprendimento di K3s, Traefik e cert-manager non si giustifica solo per fare reverse proxy.

I Problemi Tipici della Configurazione

Configurare un reverse proxy in Kubernetes non è lineare come farlo con Nginx o Caddy. Ci sono diversi punti in cui la configurazione può rompersi senza messaggi di errore chiari.

Middleware inesistenti o mal referenziati

Traefik referenzia i middleware con il formato <namespace>-<nome>@kubernetescrd. Se il nome o il namespace non corrispondono esattamente a quanto dichiarato nel manifest del Middleware, Traefik non trova la risorsa e restituisce 404 sull’intera route, non un errore esplicito, ma una pagina vuota. Spesso il log è meno evidente e bisogna abilitare un verbosing approfondito di Traefik.

Endpoints deprecati

Kubernetes ha deprecato la risorsa v1 Endpoints a partire dalla versione 1.33. La sostituzione e EndpointSlice (discovery.k8s.io/v1). Se il cluster è aggiornato l’uso di Endpoints genera warning e potrebbe non funzionare in futuro. Il passaggio richiede l’aggiunta della label kubernetes.io/service-name sull’EndpointSlice per collegarlo al Service.

Content Security Policy troppo restrittiva

Una CSP che blocca blob: o data: negli img-src e media-src puo impedire il funzionamento di interfacce web che caricano contenuti dinamici. Un media server, ad esempio, genera URL blob: tramite MediaSource Extensions per la riproduzione video nel browser. Se la CSP non li consente, il player non funziona e non esiste nessun errore visibile nella pagina.

Compressione di stream compressi

Abilitare la compressione Traefik con compress: {} senza esclusioni significa che anche stream video e audio codificati con codec lossy come H.264 o AAC vengono processati da gzip o brotli. Il risultato è un aumento del consumo CPU senza riduzione apprezzabile della dimensione dei dati. Per i MIME types da escludere, usare sempre tipi completi (video/mp4, video/x-matroska) e mai prefissi troncati come video/, che alcune versioni di Traefik non gestiscono correttamente.


Implementazione: Esporre un Servizio Esterno

Prendiamo come esempio un servizio che gira su un container con IP 10.0.0.20 sulla porta 3000, da esporre sul dominio git.example.org.

1. Namespace

Ogni servizio ha il proprio namespace. Questo isola le risorse e permette di applicare middleware con scope per-app.

apiVersion: v1
kind: Namespace
metadata:
  name: myservice

2. Service headless

Il Service dichiara le porte ma non ha selector — non punta a Pod nel cluster, ma verra collegato manualmente a un IP esterno tramite EndpointSlice.

apiVersion: v1
kind: Service
metadata:
  name: myservice
  namespace: myservice
spec:
  ports:
    - name: http
      port: 3000
      targetPort: 3000

3. EndpointSlice

Questa risorsa collega il Service all’IP del container esterno. La label kubernetes.io/service-name e obbligatoria per il binding.

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: myservice-1
  namespace: myservice
  labels:
    kubernetes.io/service-name: myservice
addressType: IPv4
ports:
  - name: http
    port: 3000
endpoints:
  - addresses:
      - "10.0.0.20"

4. Certificato TLS

cert-manager gestisce il rinnovo automatico tramite Let’s Encrypt con challenge DNS-01 via Cloudflare.

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: myservice-tls
  namespace: myservice
spec:
  secretName: myservice-tls
  issuerRef:
    name: letsencrypt-cloudflare
    kind: ClusterIssuer
  dnsNames:
    - git.example.org

5. Middleware di sicurezza

Un middleware che applica HSTS, CSP, XSS filter e Permissions-Policy. Questo vive nel namespace del servizio e viene referenziato come myservice-myservice-headers@kubernetescrd.

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: myservice-headers
  namespace: myservice
spec:
  headers:
    contentSecurityPolicy: >-
      default-src 'self';
      script-src 'self' 'unsafe-inline';
      style-src 'self' 'unsafe-inline';
      img-src 'self' data:;
      font-src 'self' data:;
      connect-src 'self';
      frame-ancestors 'none';
      base-uri 'self';
      form-action 'self';
      upgrade-insecure-requests;
    contentTypeNosniff: true
    customFrameOptionsValue: "DENY"
    forceSTSHeader: true
    stsSeconds: 31536000
    stsIncludeSubdomains: true
    stsPreload: true
    browserXssFilter: true

6. Ingress

L’ingress collega tutto: host, TLS, backend e catena di middleware.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myservice
  namespace: myservice
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls: "true"
    traefik.ingress.kubernetes.io/router.middlewares: >-
      myservice-myservice-headers@kubernetescrd,
      kube-system-rate-limit@kubernetescrd
spec:
  ingressClassName: traefik
  tls:
    - hosts:
        - git.example.org
      secretName: myservice-tls
  rules:
    - host: git.example.org
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: myservice
                port:
                  number: 3000

Perché Questo Stack

K3s fornisce Traefik di default. cert-manager automatizza i certificati. I middleware Traefik applicano header di sicurezza. L’insieme funziona come un reverse proxy centralizzato con TLS e hardening, senza installare software aggiuntivo sui singoli host.

Il vantaggio pratico è la centralizzazione: una modifica al middleware di rate limiting si propaga a tutti i servizi che lo referenziano. Un rinnovo certificato gestito da cert-manager non richiede intervento manuale. Un nuovo servizio esterno si aggiunge con cinque o sei manifest YAML e un kubectl apply.

Il costo è la complessità intrinseca di Kubernetes. Per due o tre servizi potrebbe non valere la pena. Nel momento in cui i servizi diventano cinque, dieci, quindici, il costo iniziale di setup viene ammortizzato dall’uniformità della gestione.


Nota: Questa configurazione espone servizi tramite un tunnel Cloudflare, non tramite porte aperte direttamente sulla macchina. Il port forwarding sul router punta al tunnel, non ai servizi. Questo aggiunge un layer di protezione contro la scansione diretta degli IP.