Dominik Miklaszewski
by Dominik Miklaszewski

Categories

Tags

Po wstępnych dywagacjach w poprzednim wpisie, pora podkasać rękawy i zabrać się do pracy. Naszymi dramatis personae będą stali bywalcy: Nomad, Consul i Vaulta oraz Traefik. Plan jest taki, aby mieć jeden ingress point (Traefik) dla mojej mikro-chmurki prywatnej, który nie tylko automatycznie wyniucha co w serwisach piszczy, a również automatycznie je obsłuży wraz z TLS.

Czym jest Traefik proxy?

Jest kawałkiem oprogramowania, którego jedynym przeznaczeniem jest rola reverse proxy. Dlaczego właśnie to on? Dlatego, że od początku autorzy postawili na integracje i automatyzację z różnymi technologiami orkiestracji i zarządzania mikro-serwisami. Jest tam docker, kubernetes (a jakże!), Nomad, Consul, Mesos itd. Przy czym wersja community - a więc darmowa, posiada dostateczne możliwości i funkcjonalności do zautomatyzowania. W przeciwieństwie do NGINX. Dla Traefika pojawienie się nowego serwisu nie wymaga restartu czegokolwiek. Traefik jako Nomad deployment skorzysta z integracji Nomada i z Consulem i z Vaultem. W taki sposób, że Nomad zapewni bezpieczne dostarczenie Consul Tokena (przechowywanego w Vaulcie) konfiguracji Traefika do integracji z Consulowym katalogiem serwisów. Dzięki temu, Traefik na bieżąco będzie w stanie obsłużyć cały cykl życia usług i aplikacji.

Logika działania Traefika w Nomadzie

Logika Traefika - jak to działa?

Koncepcja przedstawiona na diagramie zamieszczonym poniżej jest następująca:

  1. Uruchomiony Traefik jako job w Nomadzie poleceniem nomad job run -check-index 0 traefik.nomad (jobspecs file tutaj)
  2. Traefik skonfigurowany zostaje tak aby dla swej statycznej, sięgnąć do Vaulta po:
    • certyfikaty TLS dla *.nukelab.home(generowanie certyfikatu opisane tutaj)
    • Consul Token celem autoryzacji dla integracji z consulowym katalogiem usług (Consul Catalog),
    • Przygotowania dynamicznej konfiguracji (docelowo ta konfiguracja powinna być usunięta z pliku jobspecs i umieszczona w GitHubie)
  3. Aplikacja myapp.nukelab.home zostaje uruchomiona w Nomadzie, z odpowiednimi tagami dla usługi (Traefik: labels), które zostaną zaprezentowane w Consulu - Nomad wszystko co się w nim uruchomi, melduje Consulowi jako usługę.
  4. Nomad jako orkiestrator - zapewnia komunikację między wirtualną siecią dokera (domyślnie ustawiona jako bridge) a interfejsem hosta, łącznie z mapowaniem i wystawieniem odpowiednich portów tcp/udp.
  5. DNS na potrzeby ćwiczenia, ma rekord dla *.nukelab.home ustawiony na IP Traefika,
  6. Traefik pobiera sobie stan katalogu usług z Consula wraz z pełną informacją o każdej usłudze,
  7. Przychodzi zapytanie o myapp.nukelab.home i dzięki w/w Traefik wie, przez który swój wewnętrzny ruter i do jakiego endpointa ma skierować ruch.

Logika działania Traefika w Nomadzie

Na plasterki!

No to poszatkujmy tego słonia na plasterki, aby kawałek po kawałku pokazać jak to wszystko zostało skonfigurowane i jak działa. Aspekt integracji Nomada, Consula i Vaulta jest poza zakresem tego wpisu. Ważne jest aby klient Nomada (agent) mógł sięgać zarówno do Vaulta jak i Consula, autoryzując się tokenami. Podpowiem, token do autoryzacji w Vaulcie Nomad ma w swoim pliku konfiguracyjnym agenta, a token do Consula przechowuje w.. Vaulcie. Zaczniemy zatem od konfiguracji Traefika.

Konfiguracja Traefika

Konfiguracja Traefika w pliku nomadowym może być zrealizowana poprzez zmienne środowiskowe, pliki konfiguracyjne YAML albo TOML oraz parametry przy odpalaniu binarki. Wykorzystamy kombinację. Parametry - przy uruchomieniu podamy traefikowej binarce ścieżkę do plików konfiguracyjnych i pliki YAML - te, wygenerujemy poprzez template nomadowy. Naszym słoniem będzie jobspecs dla Traefika.

Zaczynamy od wiersza 19, gdzie zaczyna się stanza group:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 
  group "traefik" {
    count = 1
    update {
      auto_revert = true
    }
    network {
      port  "http" {
        to     = 80 
        static = 80
      }
      port "https" {
        to     = 443
        static = 443
      }
      port  "api" {
        to     = 8080
        static = 8080
      }
      dns {
        servers = ["192.168.120.231"]
      }
    }

 

W tym miejscu mapujemy i wystawiamy do sieci porty Traefika, w jego nomenklaturze zwane entrypointami. Można jeszcze wystawić metryki na port 8082/tcp, ale na nie jest to niezbędne na potrzeby naszego ćwiczenia. Dalej będzie już tylko ciekawiej.

Od wiersza 44:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 
    service {
      port = "https"
      tags = [
          "traefik",
          "traefik.enable=true",
          "traefik.http.routers.dashboard.rule=Host(`traefik.nukelab.home`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))",
          "traefik.http.routers.dashboard.tls: true",
          "traefik.http.routers.dashboard.service=api@internal"
      ]
      check {
       type     = "tcp"
       interval = "15s"
       timeout  = "5s"
      }
    }

    service {
      tags = ["lb", "api"]
      port = "api"
 
      check {
        type     = "http"
        path     = "/ping"
        interval = "15s"
        timeout  = "5s"
      }
    }
 

W ramach grupy traefik i wystawionych portów, definiujemy usługi, które Nomad zamelduje Consulowi. Pierwsza usługa z portem https to sam on - Traefik. Sam dla siebie musi wiedzieć, że jak ktoś wyśle zapytanie o https://traefik.nukelab.home/api|dashboard to ma odpowiedzieć własnym dashboardem.

  • "traefik", tag, który oznacza usługę, która ma być wystawiona przez Consula do Traefika
  • "raefik.enable=true", tag potwierdzający gotowość do usługi do obsługi (sic!) przez Traefika
  • "traefik.http.routers.dashboard.rule=Host(traefik.nukelab.home) && (PathPrefix(/api) || PathPrefix(/dashboard))" - tag definiujący ruter i reguły sterujące tym ruterem,
  • "traefik.http.routers.dashboard.tls: true", tag, który mówi tyle, że wszystko co przechodzi przez ten ruter ma być obłużone przez HTTPS,
  • "traefik.http.routers.dashboard.service=api@internal", serwis traefikowy który ma być obsłużony przez ten ruter

Druga usługa to api. Obie usługi mają zdefiniowane checki - też dla Consula, aby ten wiedział czy usługi żyją i są zdrowe. A więc tu dochodzimy do ważnej obserwacji - w Nomadzie, aby dana aplikacja czy cokolwiek innego (MongoDB, czy inny mikro-serwis) był widoczny i obsłużony przez nasze rev-proxy, musi mieć odpowiednie tagi w definicji service.

Od wiersza 74:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 
task "proxy" {
      driver = "docker"
      config {
#        network_mode  = "host"
        command       = "traefik"
        args          = [ "--configFile", "/local/traefik.yml" ]
        image         = "powernuke.nukelab.home:5443/traefik:2.8.4-8"
        ports         = ["api", "http", "https"]
      }
      vault {
        policies = ["traefik-access"]
      }
 

Tutaj standard, Nomad po prostu każe dockerowi ściągnąć obraz, odpalić z zadanymi argumentami i wystawić uprzednio zdefiniowane porty. Dzięki integracji z Vaultem, na końcu pojawia się deklaracja, że w tym przypadku agent nomadowy będzie autoryzowany przez politykę Vaultową o nazwie traefik-access do sięgnięcia po sekrety w Vaulcie, w ścieżce przeznaczonej dla Traefika. Obraz Traefika mam swój - żadna filozofia, alpine 3.16 z binarką Traefika + entrypoint.sh.

Dalej, od wiersza 89 do 134:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 
      template {
       data        = <<EOH
tls:
  stores:
    default:
      defaultCertificate:
        certFile: /local/traefik.crt
        keyFile: /local/traefik.key
EOH
       destination = "/local/dynamic.yml"
       change_mode = "restart"
       splay       = "1m"
      }
 

Budujemy tutaj konfigurację dynamiczną, którą poprzez konfigurację statyczną, Traefik sobie “w locie” sprawdza, czy nie ma jakichś zmian. Ta jest inicjalna, więc może tu być, ale np. inne certyfikaty, czy inne specyficzne sprawy dla danej usługi wypada obsługiwać przez kontrolę wersji w GitHubie i po przez GH Action zasilać ten katalog local.

1
2
3
4
5
6
7
8
9
10
11
12
13
 
      template {
        data        = <<EOH
{{ with secret "kv/data/traefik/nukelab" }}
{{ .Data.data.certkey }}
{{ end }}
EOH
        destination = "/local/traefik.key"
        change_mode = "restart"
        splay       = "1m"
      }
      [..]
 

Mamy trzy instancje template. Po kolei - pierwsza wyciąga z Vaulta klucz do certyfikatu dla *.nukelab.home, druga sam certyfikat, a trzecia CA. Przy czym, znalazłem tutoriala, gdzie autor miał jakieś problemy i jemu zadziałało to tylko wtedy kiedy miał cert fullchain, łącznie z kluczem (czyli: klucz, cert, intermediate, ca). U mnie działa poprawnie, a nie działało jak sugerował :-) Sekrety wyciągnięte z Vaulta lądują w lokalnym katalogu kontenera z Traefikiem w /local. Można podejrzeć w Nomadzie (o ile ma się token dostępowy admina - rzecz jasna):

Na koniec konfiguracja statyczna Traefika

Od wiersza 138:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 
      template {
        data = <<EOH
{{ with secret "kv/data/traefik/nukelab" }}
serversTransport:
  insecureSkipVerify: true
entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"
api:
  dashboard: true
  insecure: false
  debug: true
ping: {}
accessLog: {}
log:
  level: DEBUG
providers:
  providersThrottleDuration: 15s
  file:
    watch: true
    directory: "/local"
  consulCatalog:
    endpoint:
      scheme: https
      address: "powernuke.nukelab.home:8501"
      datacenter: nukelab
      token: {{ .Data.data.consultoken | toJSON}}
      tls:
        ca: /local/ca.crt
        cert: /local/traefik.crt
        key: /local/traefik.key
        insecureSkipVerify: true 
    cache: false
    prefix: traefik
    connectAware: true
    exposedByDefault: false
    watch: true
{{ end }}
EOH

       destination = "local/traefik.yml"
       change_mode = "noop"
 

Template w tym wypadku ściąga nam token Consula, dlatego występuje. Jadąc od góry:

  1. Definicja entrypointów - porty dla HTTP i HTTPS,
  2. Dostępność dashboardu Traefika po HTTPS,
  3. Ustawienie poziomu logowania zdarzeń na DEBUG,
  4. Najważniejsza rzecz: providers - file i tutaj Traefik dynamicznie obserwuje katalog /local oraz..
  5. consulCatalog - gdzie definiujemy rodzaj komunikacji HTTP/HTTPS (w naszym przypadku), URL Consula, token i ścieżki do certyfikatów TLS - Consul sprawdza poprawność certyfikatów klienta. Ważne by mieć definicję providers file bez tego nie zadziała nam HTTPS, po prostu Traefik nie będzie wiedział, gdzie ma pliki z certami,
  6. Na koniec prefix czyli tag jaki musi mieć każda usługa zdeployowana w Nomadzie, którą chcemy udostępnić przez Traefika, connectAware czy obsługiwać Consul Connecta (jeszcze nie do końca wiem co robi, ale nie przeszkadza), i exposedByDefault ustawiony na false - czyli nie chcemy aby Consul udostępniał Traefikowi wszystkiego co ma, a tylko te usługi otagowane “traefik”.

Poniżej pliki konfiguracyjne deploymentu Traefika widziane przez konsolę Nomada:

Traefik Nomad

Zmiany w aplikacji

W nomadowym pliku aplikacji zmieniło się tylko to, że doszły tagi w definicji usługi (service stanza) - wiersz 63:

1
2
3
4
5
6
7
8
 
      tags = [
        "traefik",
        "traefik.enable=true",
        "traefik.http.routers.myapp.rule=Host(`myapp.nukelab.home`)",
        "traefik.http.routers.myapp.tls=true",
      ]
 

Tak jak przy objaśnieniu konfiguracji samego Traefika, znaczą one tyle:

  1. Usługa udostępniana Traefikowi przez Consula,
  2. Ruter myapp ma obsługiwać tylko zapytania dla myapp.nukelab.home
  3. i tylko poprzez entrypoint HTTPS

Jest też testowa usługa whoami

Jak to wygląda?

  • Dashboard:

Traefik Dashboard

  • Informacja o ruterze dla myapp:

Traefik Dashboard

  • Informacja o serwisie myapp:

Traefik Dashboard

  • Tagi aplikacji w Consulu:

Tags in Consul

  • Obsługa aplikacji przez HTTPS:

Aplikacja

Podsumowanie

Mamy rev-proxy, który zapewnia nam bezpieczeństwo serwisów uruchomionych na naszej platformie. A także, jakiś elementarny porządek. Oczywiście, w środowisku produkcyjnym, wildcard cert nie jest mile widziany i naprawdę nie wypada w dzisiejszych czasach nie mieć PKI czy jakiegoś Sectigo dla certów. Prócz tego, w Traefiku możemy też skonfigurować integrację z CrowdSec czy plugin Souin (HTTP cache z ciekawymi opcjami). Przedstawioną wyżej konfigurację można zrealizować pewnie bardziej elegancko, bądź w ogóle inaczej - sekrety wstawić do GitHub Secrets pliki konfiguracyjne osobno i tylko templetami wciągać do jobspecs, a dla pełnej dynamiki katalog /local po prostu podmontować jako host mounted catalog w tasku (też jobspecs).

Co jeszcze?

Warto byłoby zabezpieczyć same serwisy w Consulu, stosując Consul intentions i Consul Connect - czyli takie reguły firewallowe regulujące ruch do/z usługi i poprzez CC - poufność tego ruchu stosując side-car proxy Envoy i TLS.

I tym sposobem dotarliśmy do końca pod-ścieżki rozwoju i zabezpieczania naszych deploymentów. Kolejne wpisy powrócą do głównego zadania jakim jest automatyzacja i testy w moim domowym CI/CD wykorzystując GH Actions. I w ramach powrotu do głównego wątku, zajmę się testowaniem różnych rozwiązań SAST/SCA. No, i oczywiście automatyka samego deploymentu.

Użyteczne sznurki: