Homelab: Certyfikaty i Dynamic DNS

W poprzednim wpisie z tego cyklu napisałem co nieco na temat postawienia k3s na prywatnym poleasingowym terminalu Della, który służy mi wiernie jako prywatny homelab.
Zainspirowany przez znajomych, rozwinę niektóre z tematów z poprzedniego wpisu i pokażę coś więcej o usługach Cloudflare oraz o zabezpieczeniu naszych usług i tajemnic.


Cloudflare Dynamic DNS

Czasami zamiast tunelowania, chcemy skorzystać bezpośrednio z wpisów A, ale… jak w mojej i wielu innych sytuacjach, mamy zmienne IP. Wtedy z pomocą przychodzi druga pożyteczna usługa w Cloudflare (która ma też inne dynamic dns odpowiedniki, jak np. DuckDNS).

W przeciwieństwie do tunelowania, konfiguracja Dynamic DNS jest nieco bardziej manualna (choć da się ją zautomatyzować) i wymaga wygenerowania tokena API z ustawieniami DNS w panelu Cloudflare. Dodatkowo warto pobrać również zoneId dla istniejącej domeny. Serwery gier itp napewno lepiej postawić w takiej formie

Tip: zoneId można pobrać w panelu Cloudflare w sekcji Overview, (Prawy dolny róg)

Konfiguracja

Na portalu Cloudflare, w sekcji subdomains ustawiamy, do jakiej subdomeny chcemy aktualizować nasze IP. Jeśli chcemy, aby połączenie szło bezpośrednio, możemy wyłączyć proxy (proxied: false).

Przykładowy plik konfiguracyjny (config.json):

{
  "cloudflare": [
    {
      "authentication": {
        "api_token": "apiToken"
      },
      "zone_id": "zoneFromCloudflare",
      "subdomains": [
        {
          "name": "www",
          "proxied": true
        }
      ]
    }
  ],
  "a": true,
  "aaaa": false,
  "purgeUnknownRecords": false,
  "ttl": "Auto"
}

Tak przygotowany plik należy zakodować do Base64:

cat config.json | base64

Następnie wkleić zakodowaną wartość do pliku config.yaml używanego przez Kubernetes.

Konfiguracja Kubernetes

---
apiVersion: v1
kind: Namespace
metadata:
  name: cloudflare-ddns
---
apiVersion: v1
data:
  config.json: zakodowany_base64_z_poprzedniego_kroku
kind: Secret
metadata:
  name: cloudflare-ddns
  namespace: cloudflare-ddns
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cloudflare-ddns
  namespace: cloudflare-ddns
spec:
  selector:
    matchLabels:
      app: cloudflare-ddns
  template:
    metadata:
      labels:
        app: cloudflare-ddns
    spec:
      containers:
      - name: cloudflare-ddns
        image: timothyjmiller/cloudflare-ddns:latest
        env:
        - name: CONFIG_PATH
          value: '/etc/cloudflare-ddns/'
        volumeMounts:
        - mountPath: '/etc/cloudflare-ddns'
          name: cloudflare-ddns
          readOnly: true
      volumes:
      - name: cloudflare-ddns
        secret:
          secretName: cloudflare-ddns

Po zaaplikowaniu ustawień możemy skonfigurować domeny w naszym domyślnym load balancerze i podpiąć certyfikat Let’s Encrypt.


Tworzenie sekretów w Kubernetes

Aby lepiej chronić wrażliwe dane (np. klucze API lub connection strings), możemy używać sekretów zamiast zmiennych środowiskowych. Zapewni nam to mniej wycieków po stronie kodu, gdzie możemy śmielej wrzucać do repozytorium naszą konfigurację kubernetesową czy helmową, ponieważ wiemy, że nasze dane są w naszej bezpiecznej krypcie.

Tworzenie sekretu

Sekrety można stworzyć prostą komendą:

kubectl create secret generic api-secrets \
  --from-literal=ConnectionStrings__Marten="wartość_marten" \
  --from-literal=Media__ConnectionStrings="wartość_media"

Weryfikacja sekretów

Możesz sprawdzić utworzone sekrety w klastrze:

kubectl get secret api-secrets

Użycie sekretów w aplikacji

Dodaj do swojego pliku Deployment odwołania do sekretów jako zmienne środowiskowe:

env:
  - name: ConnectionStrings__Marten
    valueFrom:
      secretKeyRef:
        name: api-secrets
        key: ConnectionStrings__Marten
  - name: Media__ConnectionStrings
    valueFrom:
      secretKeyRef:
        name: api-secrets
        key: Media__ConnectionStrings

Let’s Encrypt na Homelab

Jeśli chcemy zautomatyzować zarządzanie certyfikatami SSL/TLS, możemy użyć narzędzia Cert Manager w Kubernetes.

Cert Manager integruje się z Let’s Encrypt, umożliwiając automatyczne wystawianie i odnawianie certyfikatów, przez co deploy trwa o wiele krócej. Nic nie stoi na przeszkodzie, by kupić certyfikat i go wstawić jako nowy secret w naszym środowisku.

  1. Instalacja Cert Managera za pomocą Helm:
   helm repo add jetstack https://charts.jetstack.io
   helm repo update
   helm install cert-manager jetstack/cert-manager \
     --namespace cert-manager \
     --create-namespace \
     --set installCRDs=true
  1. Utworzenie sekreta z API tokenem Cloudflare:
   kubectl create secret generic cloudflare-api-token \
     --namespace cert-manager \
     --from-literal=api-token=<YOUR_CLOUDFLARE_API_TOKEN>
  1. Tworzenie ClusterIssuer:
   apiVersion: cert-manager.io/v1
   kind: ClusterIssuer
   metadata:
     name: lifelike-cert
   spec:
     acme:
       email: <your-email@example.com>
       server: https://acme-v02.api.letsencrypt.org/directory
       privateKeySecretRef:
         name: cluster-issuer-account-key
       solvers:
       - dns01:
           cloudflare:
             email: <your-cloudflare-email@example.com>
             apiTokenSecretRef:
               name: cloudflare-api-token
               key: api-token
  1. Podpięcie certyfikatu w Ingress:
   apiVersion: networking.k8s.io/v1
   kind: Ingress
   metadata:
     name: coffeerecipes-api-ingress
     annotations:
       traefik.ingress.kubernetes.io/router.entrypoints: websecure
       traefik.ingress.kubernetes.io/router.tls: "true"
       cert-manager.io/cluster-issuer: lifelike-cert
   spec:
     tls:
     - hosts:
         - dev-api-recipes.lifelike.cloud
       secretName: lifelike-cloud-tls
     rules:
     - host: dev-api-recipes.lifelike.cloud
       http:
         paths:
         - path: /
           pathType: Prefix
           backend:
             service:
               name: coffeerecipes-api-service
               port:
                 number: 80

Kod na koniec:

Aby podsumować działanie w tej części i nie irytować się, gdzie kod i wszystko

apiVersion: apps/v1
kind: Deployment
metadata:
  name: coffeerecipes-api
  labels:
    app: coffeerecipes-api
spec:
  replicas: 1
  selector:
    matchLabels:
      app: coffeerecipes-api
  template:
    metadata:
      labels:
        app: coffeerecipes-api
    spec:
      containers:
        - name: coffeerecipes-api
          image: aluspl/coffeerecipesapi:develop
          imagePullPolicy: Always
          ports:
            - containerPort: 80
          env:
            - name: ConnectionStrings__Marten
              valueFrom:
                secretKeyRef:
                  name: api-secrets
                  key: ConnectionStrings__Marten
            - name: Media__ConnectionStrings
              valueFrom:
                secretKeyRef:
                  name: api-secrets
                  key: Media__ConnectionStrings
            - name: ASPNETCORE_URLS
              value: "http://*:80"
          livenessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 30
            periodSeconds: 10
          resources:
            limits:
              memory: "1024Mi"
              cpu: "500m"
            requests:
              memory: "512Mi"
              cpu: "250m"

---

# Service dla aplikacji .NET
apiVersion: v1
kind: Service
metadata:
  name: coffeerecipes-api-service
spec:
  selector:
    app: coffeerecipes-api
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
      name: "http"
---

# Ingress dla aplikacji z użyciem Traefik
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: coffeerecipes-api-ingress
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls: "true"
    cert-manager.io/cluster-issuer: lifelike.cloud
spec:
  tls:
    - hosts:
        - dev-api-recipes.lifelike.cloud
      secretName: lifelike-cloud-tls
  rules:
    - host: dev-api-recipes.lifelike.cloud
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: coffeerecipes-api-service
                port:
                  number: 80

Podsumowanie

Mam nadzieję, że wprowadzone zmiany i dodatkowe wskazówki będą pomocne w zabezpieczeniu Waszych homelabów. Dzięki za rady po poprzednim wpisie – dzięki nim udało mi się dopracować ten cykl.

Jak zwykle, zapraszam do spojrzenia, pouczenia – bo ja sam się dopiero uczę.

Źródła

Poniżej źródła, które pomogły mi w tym artykule.

https://nolifelover.medium.com/create-cert-manager-clustterissuer-with-cloudflare-for-automate-issue-and-renew-lets-encrypt-ssl-4877d3f12b44

https://cavecafe.medium.com/homelab-cloudflare-ddns-setup-09b37b54a7fb

https://nolifelover.medium.com/create-cert-manager-clustterissuer-with-cloudflare-for-automate-issue-and-renew-lets-encrypt-ssl-4877d3f12b44