Blog

Zmień ustawienie bez wdrożenia: klucz w Consul KV z ACL

Czasem potrzebujesz jednego małego fragmentu konfiguracji, który jest (a) taki sam we wszystkich działających instancjach aplikacji, (b) przetrwa restarty i (c) da się zmienić w czasie działania — bez przebudowy i ponownego wdrożenia. Flaga funkcji. Przełącznik trybu konserwacji. Globalne ustawienie strony. Sięganie po bazę danych albo przebudowę pliku konfiguracyjnego to przerost formy nad treścią.

Jeśli już używasz Consula, jego magazyn KV to naturalne miejsce — a ten wpis pokazuje, jak zrobić to bezpiecznie, gdy ACL Consula są włączone (a powinny być).

Dlaczego Consul KV

  • Współdzielony. Agent na każdym węźle rozmawia z tym samym KV, więc wszystkie repliki czytają jedną wartość.
  • Trwały. Wartość żyje w stanie Consula — przetrwa restarty.
  • Z nasłuchem. Blocking queries przesyłają zmiany do aplikacji niemal w czasie rzeczywistym — bez odpytywania.
  • Już jest. Jeśli używasz Consula do service discovery, magazyn KV masz w pakiecie.

Haczyk z ACL (i dlaczego to dobrze)

Dobrze prowadzony Consul ma włączone ACL z default_policy = deny. Twoja aplikacja nie odczyta ani nie zapisze KV, dopóki nie wręczysz jej tokenu — i to zaleta, nie utrudnienie. Właściwe podejście to minimum uprawnień: token, który może dotknąć dokładnie tego jednego prefiksu klucza, którego potrzebuje, i niczego więcej.

Krok 1 — wąsko zakresowa polityka

Polityka to zbiór reguł. Nadaj prawo zapisu tylko na swój prefiks:

# myapp-config.hcl
key_prefix "myapp/config/" {
  policy = "write"   # "write" obejmuje też odczyt
}
consul acl policy create -name myapp-config -rules @myapp-config.hcl

Prefiks kończy się ukośnikiem / i jest właściwy dla Twojej aplikacji. Nie nadawaj "" — to całe drzewo KV.

consul acl policy create to jednorazowy upload, a nie plik konfiguracyjny, który gdzieś umieszczasz. .hcl to tylko wejście, które raz podajesz CLI — tak jak plik .sql jest wejściem dla bazy danych. Przepływ:

  piszesz       myapp-config.hcl       (reguły, w pliku tekstowym — gdziekolwiek)


  consul acl policy create -rules @myapp-config.hcl
       │   (CLI czyta plik i WYSYŁA reguły do Consula przez jego API HTTP)

  Consul zapisuje politykę we WŁASNYM stanie wewnętrznym
       (baza Raft na węzłach *serwerowych* Consula — replikowana, nie Twój plik)

Po wykonaniu tego polecenia polityka jest danymi wewnątrz Consula, identyfikowanymi po nazwie i ID; plik spełnił swoje zadanie. Czyli:

  • Gdzie umieścić plik, żeby Consul go użył? Nigdzie. Consul nie obserwuje żadnego katalogu w poszukiwaniu plików polityk — nie wrzucaj go do /etc/consul.d/ w nadziei, że się wczyta. Plik to jednorazowe wejście, tak samo jak .sql jest wejściem dla psql: baza zapisuje wiersz we własnych tabelach i nigdy więcej nie czyta Twojego .sql.
  • Skąd Consul wie, gdzie on jest? Nie szuka pliku. Gdy token (z dołączoną tą polityką) wysyła żądanie, Consul odnajduje politykę po ID we własnym stanie — żaden odczyt pliku się nie odbywa.
  • Który serwer ją ma? Wszystkie węzły serwerowe Consula, jako część replikowanego stanu Raft — jako wewnętrzne dane Consula, a nie kopia Twojego .hcl.

Więc trzymaj .hcl w systemie kontroli wersji jako źródło i możesz je skasować z dysku w dowolnej chwili — działająca polityka tego nie zauważy.

Nazewnictwo klucza

Przydatny schemat to <zakres>/<aplikacja>/<nazwa> (np. prod/billing/rate-limit): zakres środowiska/projektu, aplikacja będąca właścicielem, a na końcu ustawienie. Małe litery, myślniki wewnątrz segmentu.

Consul KV to płaski magazyn — nie ma w nim prawdziwych folderów. Gdy zapisujesz myapp/config/feature-x, Consul nie tworzy folderu myapp zawierającego folder config; przechowuje jeden wpis, którego kluczem jest dosłowny ciąg myapp/config/feature-x. / to tylko znak w tym ciągu — dla silnika magazynu nie bardziej wyjątkowy niż - czy litera (mógłbyś nazwać klucz myapp:config:x, a Consulowi byłoby wszystko jedno). Hierarchia to konwencja: interfejs, kv get -recurse (polecenie wypisujące wszystkie klucze pod daną ścieżką) i reguły key_prefix (reguły ACL nadające dostęp do całej grupy kluczy naraz) traktują / jako separator i dopasowują po prefiksie ciągu — więc konsekwentne nazewnictwo z / sprawia, że powiązane klucze grupują się, listują i są obejmowane uprawnieniami razem, tak jakby leżały w folderach.

Jedno rozróżnienie ma znaczenie. Ścieżka kończąca się / (myapp/config/) to prefiks: sam nie przechowuje wartości — reprezentuje całą grupę kluczy, które się od niego zaczynają, np. myapp/config/feature-x, myapp/config/theme, myapp/config/rate-limit. Wskazujesz na niego politykę albo listowanie rekurencyjne, żeby zadziałać na wszystkich naraz — nadać uprawnienie całej grupie albo odczytać każdy klucz pod nim jednym wywołaniem. Ścieżka bez końcowego ukośnika (myapp/config/feature-x) to klucz-liść: to wpis, który faktycznie przechowuje wartość. Odpowiada to dwóm sposobom adresowania kluczy w polityce — zbiorczo albo pojedynczo: key_prefix "myapp/config/" nadaje uprawnienia na każdy klucz pod prefiksem (obecny i przyszły), a key "myapp/config/feature-x" — tylko na ten jeden, konkretny klucz. Sięgając po regułę prefiksową, ustawiasz politykę raz — a potem dodajesz pod nią dowolnie wiele kluczy bez ponownego ruszania polityki, wszystkie objęte tym jednym nadaniem.

Ta sama ścieżka klucza pojawia się w więcej niż jednym miejscu: w polityce ACL — która musi nadać tokenowi dostęp do dokładnie tej ścieżki — oraz w jednorazowym poleceniu ustawiającym wartość początkową (consul kv put …) i w odczytach, zapisach i nasłuchu Twojej aplikacji. Wszystkie muszą zapisać ścieżkę identycznie, a niezgodność zawodzi po cichu, nie głośno.

Najłatwiej pomylić się w polityce, bo jej prefiks i klucz to ten sam ciąg w dwóch rolach: myapp/config/ (prefiks polityki z Kroku 1) to czego token może dotknąć, a myapp/config/feature-x (klucz aplikacji) to czego dotyka aplikacja. Prefiks musi obejmować klucz. Pomyl którąkolwiek stronę tak, że przestanie obejmować, a Consul zwróci odmowę dostępu. Pomyl tylko odczyt w aplikacji, a dostaniesz pusty wynik — nieistniejący klucz to nie błąd. Tak czy inaczej nic się nie wywala; ustawienie po prostu po cichu nigdy nie zaczyna działać, a ty szukasz literówki.

Krok 2 — wygeneruj token dla tej polityki

Zarówno to polecenie, jak i consul acl policy create z Kroku 1 to komendy administracyjne — zmieniają sam system ACL Consula, więc wymagają tokenu zarządzającego, a nie zakresowego tokenu, który dopiero tworzysz. Bez niego, przy default_policy = deny, zwrócą 403.

Token zarządzający to token przypięty do wbudowanej w Consula polityki global-management — pełny, nieograniczony dostęp do całego klastra (stąd właśnie bierze się acl = "write"). Nie tworzysz go na potrzeby tego zadania; on już istnieje. Ten pierwszy to token bootstrap, który consul acl bootstrap wypisuje przy pierwszym włączeniu ACL w klastrze — operator trzyma go w magazynie sekretów (i może z tej samej polityki wygenerować kolejne tokeny zarządzające). To poświadczenie operatorskie używane do konfiguracji, a nie coś, co dostarczasz wraz z aplikacją.

CLI consul uwierzytelnia własne żądanie do klastra tokenem ze zmiennej środowiskowej CONSUL_HTTP_TOKEN (albo z flagi -token=) — to po prostu poświadczenie, które CLI przedstawia, pełniące tę samą rolę co nagłówek X-Consul-Token w API HTTP w dalszych krokach. Najpierw wyeksportuj token zarządzający:

export CONSUL_HTTP_TOKEN="$MGMT_TOKEN"   # token zarządzający (acl = "write"); Krok 1 też go potrzebował
consul acl token create -description "myapp config" -policy-name myapp-config

Polecenie wypisuje dwa identyfikatory:

  • AccessorID — publiczny uchwyt, do późniejszego wglądu lub unieważnienia tokenu.
  • SecretID — właściwy token. Traktuj go jak hasło.

Przechwyć SecretID prosto do swojego magazynu sekretów. Nigdy nie wypisuj go do logu ani nie commituj do repozytorium.

Krok 3 — zapisz sekret tam, skąd aplikacja czyta sekrety

Umieść SecretID tam, skąd aplikacja już pobiera sekrety — menedżer sekretów, magazyn zmiennych/sekretów Twojego orkiestratora, wstrzyknięta zmienna środowiskowa. Nie w repozytorium. Aplikacja odczyta go (np. CONSUL_TOKEN) przy starcie.

Krok 4 — wartość początkowa

Przy włączonych ACL ten zapis też wymaga tokenu — samo consul kv put zwróci 403. Pochodzi on z tej samej zmiennej CONSUL_HTTP_TOKEN co w Kroku 2, ale tutaj może to być dowolny token zapisujący w prefiksie: Twój token zarządzający albo zakresowy token, który właśnie utworzyłeś (ma on już write na myapp/config/).

export CONSUL_HTTP_TOKEN="$SECRET_ID"   # SecretID z Kroku 2 (lub token operatorski)
consul kv put myapp/config/feature-x on

Krok 5 — odczyt i zapis z aplikacji

Nie potrzebujesz ciężkiej biblioteki klienckiej — API HTTP w zupełności wystarczy:

# odczyt
curl -s -H "X-Consul-Token: $TOKEN" \
  http://127.0.0.1:8500/v1/kv/myapp/config/feature-x?raw

# zapis
curl -s -X PUT -H "X-Consul-Token: $TOKEN" \
  --data 'off' http://127.0.0.1:8500/v1/kv/myapp/config/feature-x

Jeśli API HTTP Consula jest serwowane po TLS, zmieniają się tylko schemat i port (HTTPS domyślnie 8501) oraz trzeba wskazać curlowi, któremu CA ufać:

# odczyt po HTTPS
curl -s -H "X-Consul-Token: $TOKEN" --cacert /etc/consul.d/tls/consul-agent-ca.pem \
  https://127.0.0.1:8501/v1/kv/myapp/config/feature-x?raw

Skieruj --cacert na CA, który podpisał certyfikat agenta (nie -k — pominięcie weryfikacji niweczy sens TLS). Nagłówek z tokenem i reszta URL są identyczne.

Lokalny agent na 127.0.0.1 pośredniczy do klastra, więc zadziała dowolny węzeł.

Krok 6 — nasłuch zmian (najlepsza część)

Odpytywanie to marnotrawstwo. Blocking queries Consula pozwalają aplikacji na long-poll: podajesz ostatni indeks i czas oczekiwania, a żądanie wisi, dopóki wartość się nie zmieni (albo nie minie czas). Przy zmianie nasłuch każdej repliki budzi się i wszystkie zbiegają się do nowej wartości:

# blokuje do 5 minut, aż klucz zmieni się powyżej ModifyIndex 42
curl -s -H "X-Consul-Token: $TOKEN" \
  "http://127.0.0.1:8500/v1/kv/myapp/config/feature-x?index=42&wait=300s"

W aplikacji: trzymaj wątek/goroutine w pętli blokujących odczytów, aktualizujący wartość w pamięci, którą ścieżka żądania czyta z zerowym opóźnieniem.

Lista kontrolna bezpieczeństwa

  • Minimum uprawnień — token zapisuje jeden prefiks, nic więcej.
  • Higiena sekretu — SecretID nigdy nie jest wypisywany, logowany ani commitowany; żyje tylko w magazynie sekretów.
  • Unieważnialny — token można w każdej chwili skasować po jego AccessorID (consul acl token delete -id <AccessorID>), a aplikacja natychmiast traci dostęp.
  • Walidacja przy odczycie — traktuj wartość z KV jak niezaufane wejście: akceptuj wyłącznie znane, poprawne wartości, a dla pozostałych wracaj do bezpiecznej wartości domyślnej.

Do czego się przydaje

  • Flagi funkcji — przełączaj zachowanie bez wdrożenia.
  • Tryb konserwacji — jeden klucz, który czyta edge/aplikacja, by pokazać baner lub zwrócić 503.
  • Pokrętła do strojenia — limity, progi, rozmiary, zmieniane na żywo.
  • Globalne ustawienia strony — przypadek, który skłonił mnie do tego wpisu.

Realny przykład: globalne ustawienie wybierane przez administratora

Niedawno spięliśmy dokładnie to dla pewnej strony. Cel: pozwolić administratorowi wybrać ustawienie z panelu tak, by każdy odwiedzający je widział — nie jako preferencję w jego przeglądarce, ale jako prawdziwą wartość domyślną dla całej witryny, zmienialną na żywo.

Schemat:

  1. Pojedynczy klucz KV przechowuje bieżącą wartość.
  2. Aplikacja ma token z prawem zapisu (jak wyżej) oraz pętlę nasłuchu, więc wszystkie repliki współdzielą jedną wartość i błyskawicznie zbiegają się przy zmianie.
  3. Aplikacja serwuje HTML witryny i wstrzykuje bieżącą wartość do strony w drodze na zewnątrz — odwiedzający dostają ją już na etapie parsowania, bez mignięcia i bez doczytywania po stronie klienta.
  4. Klucz zapisuje wyłącznie panel za logowaniem (dostępny tylko dla administratora).

Efekt: administrator zmienia jeden element sterujący, klucz KV się aktualizuje, nasłuch rozsyła to do każdej repliki, a kolejna strona, którą wczyta dowolny odwiedzający, już to odzwierciedla — bez przebudowy, bez ponownego wdrożenia, identycznie dla wszystkich i z przetrwaniem restartów. Wszystko z jednego klucza zabezpieczonego ACL.

Podsumowanie

Dla jednego małego, współdzielonego, zmienialnego na żywo ustawienia nie potrzebujesz bazy danych ani potoku wdrożeniowego. Pojedynczy klucz Consul KV zabezpieczony ACL — token o minimalnych uprawnieniach, nasłuch przez blocking query, walidacja przy odczycie — daje konfigurację działającą w czasie rzeczywistym w całym klastrze, przy minimalnym narzucie. Dyscyplina, która czyni to bezpiecznym, to ta sama dyscyplina, która czyni to nudnym: wąsko zakreśl token, trzymaj sekret poza kodem i traktuj wartość jak niezaufane wejście.

← Wróć na blog

Komentarze