Proof of Concept – secure SSH keys store lub config store
Napisał: Patryk Krawaczyński
06/10/2014 w Administracja Brak komentarzy. (artykuł nr 461, ilość słów: 1900)
Z
ałożenie projektu: stworzyć usługę, która w (dość?) bezpieczny sposób będzie przechowywała i udostępniała (wrażliwe) dane. “Nice to have” (Fajnie to mieć) to możliwość jej skalowania, wysoka dostępność, dobra wydajność oraz self-service . Jako backendu usługi użyjemy etcd – wysoko dostępnej bazy typu key – value używanej m.in. do współdzielonej konfiguracji oraz wykrywania usług (ang. service discovery). Na froncie użyjemy serwera Varnish, który za pomocą kilku wtyczek pozwoli nam wprowadzić mechanizm uwierzytelniania oraz swego rodzaju API.
1. Front API:
Załóżmy, że w naszej usłudze – nazwijmy ją evrhstore chcemy przechować prywatny klucz SSH, który później pobierzemy w innym mechanizmie do dalszego wykorzystania. Zacznijmy, więc od uzyskania tokenu, którym będziemy później się uwierzytelniać. Usługa pozwala nam na samodzielne zakładanie “konta” – wystarczy, że w nagłówku HTTP X-Secret prześlemy tajne hasło na bazie, którego zostanie wygenerowany token. Token generowany jest za pomocą wtyczki vmod_digest – metodą digest.hmac_sha256.
set req.url = regsub(req.url, "^/get_token$", "/version"); curl.get("http://127.0.0.1:4002/v2/keys/" + digest.hmac_sha256(var.get_string("salt"),\ std.toupper(req.http.X-Secret))); if (curl.status() == 200) { error 400 "Secret already used - choose diffrent"; } else { curl.post("http://127.0.0.1:4002/v2/keys/" + digest.hmac_sha256(var.get_string("salt"),\ std.toupper(req.http.X-Secret)), "dir=true"); } return (pass);
W celu dodatkowego zabezpieczenia tej procedury po stronie serwera Varnish (na przykład, gdyby hasło X-Secret zostało odgadnięte / przejęte) – generowany jest losowy klucz (vmod_var + vmod_std), który dołączany jest do frazy X-Secret. W ten sposób użycie tego samego sekretu spowoduje wygenerowanie innego tokena.
sub random_salt { var.set_string("salt", "Johnny.Five" + std.random(31337, 73313)); }
Mimo to dla pewności przed zwróceniem tokenu sprawdzane jest (vmod_curl) jego występowanie w bazie, aby nie doszło do sytuacji przyznania tej samej przestrzeni dla użytkownika. Pobierzmy zatem nasz token:
curl -v -H 'X-Secret: Short.Circuit' http://evrhstore/get_token * About to connect() to evrhstore port 80 (#0) * Trying 192.168.1.1... connected > GET /get_token HTTP/1.1 > User-Agent: curl/7.22.0 > Host: evrhstore > Accept: */* > X-Secret: Short.Circuit > < HTTP/1.1 200 OK < Content-Type: text/plain; charset=utf-8 < X-Token: 0x6a6d3bc919eaa2a5d7e1118be3b9d7b50cb490af6ad3dd092e5c601cf04162fb < Content-Length: 10 < Accept-Ranges: bytes < Date: Wed, 24 Sep 2014 21:14:47 GMT < Connection: keep-alive < X-Cache: MISS < * Connection #0 to host evrhstore left intact * Closing connection #0
Co się stanie, jeśli w metodzie /get_token nie prześlemy nagłówka X-Secret?
* About to connect() to evrshstore port 80 (#0) * Trying 192.168.1.1... connected > GET /get_token HTTP/1.1 > User-Agent: curl/7.22.0 > Host: evrshstore > Accept: */* > < HTTP/1.1 401 Unauthorized < Server: Varnish < Accept-Ranges: bytes < Date: Wed, 24 Sep 2014 21:36:23 GMT < Connection: close < X-Cache: MISS < * Closing connection #0
To samo tyczy się nieprawidłowego nagłówka Host
:
if (req.http.host == "evrshstore") { if (req.request == "GET" && req.url ~ "^/get_token$") { if (!req.http.X-Secret) { error 401 "Unauthorized"; } } else { error 404 "Unknown virtual host"; }
Skoro usługa przyznała nam token, który de facto jest niczym innym jak katalogiem w etcd, w którym możemy przechowywać wiele kluczy z różnymi wartościami – umieśćmy nasz klucz SSH za pomocą metody set_key?name= (PUT):
curl -v -H 'X-Token: 0x6a6d3bc919eaa2a5d7e1118be3b9d7b50cb490af6ad3dd092e5c601cf04162fb'\ -L http://evrhstore/set_key?name=ssh_key -XPUT --data-urlencode "value=\ -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEApFYqFHx9zejQNV6Jx5JPz2WTnF22x0vdkDMPyMQqRWMior+B NfQTVdvGBZNW01C/NrCpldIGqg2Zh8UjVQBbzJwuewK5DmBcZfaBZLQ0B3q30ixr 6iLQ1lgtcfbNhpTELZ0+I0anrT5o4zQFKzHM4u5r806yCjr/lwN/WgA5fo0keGVH 0dmER/32vclGosuszQdNyBGI+PifxHX7Iy3SuI641OTptrCdQCQuYQZzocnzbFaw vRuOtHMqfq/zmVM1SI9LQw9qOobAMmpKTbGXqy1sUcDUNxANurLDqRCJW7BTjaKl SZelRckCLrN2Fr2U6SyuvFe2U2Snibc8H9OZhwIDAQABAoIBAQCJASCx1ZvYR4kV hUxealdJM4jdaq+P9Wqw5jD5krkfAegFQJzLS0G3abLsQQq4v2+6e4vWULOnoBDo RO0Q12yw52BEw19eYe2GP/1d5HIf7ipD+S66ku4CVJ8GjNiJo4rt8FK2fBgccZRm Hp9UhH8ojdbHkEsUBl3BG3RXpypHmrPVDdvKd5rJ8NMsxfwMPWOH/y5dU6yvpaVo AB4Fv7TRxpuWb47gI3fqsYT86+Vid5QRY/Cr+urBLeK2WK7T8trGBpiZBKwgVi4o vQJIfepbPDGRYzw339F2S0JzEk/U7rFhXu02OtJ/npqhaHTgI6H+31r6lAV/U8cg kvC0XqeZAoGBAM5CXLLX46TBI/Wyx87+apCcLikk7JQAXA9CLHI3OOQtp3mIGslT it4ilgDpdDjA31NtM5eolMdEG2kqY+jehPB/jkjo03tn14rdYRJSnKo2022lJYCS AdCE7kTUiAzzbxp7qD33Kc96uO+aTkeoTDvtCUXDoHSSSxfPUKxQnj/dAoGBAMv3 qXjp4fttGh/KegjhmP6qdf8z40ClB9aLV4CLcl0lQTYVXBnR1w3HbzVI+YnZuESK hTm4M4PLVOSTtVU9YcOOSK4Nphre/Kzxdtz9xmPzBntgE0DQNpuHK2YfRuFmwfWc /DbSa/65MSHP5BOdGlx5JR71zmEZNEHSOKNeqpqzAoGAZ2OmedNz/br+3oMuWxj2 q+RN0zv6Broja1adVudNcjtcTrQl0TM6Udz+WhirtGMhzvzXNrc/VJ9UKiQgjrMz 6iXWWb+zepFz3tzXcsrMUxpGYxi8MrV9iVuI4CG0zOEdmSXRELabU7BNkXVEtfCr vgI0eq7z+Fr4n0fBRY7ntFUCgYAZIm/T4p5iaVNqz3yyU3qTB1Z7GF3MvYl1ur4R rR1utQSQMZmj5OPnYsglfNSjVB0M9TTto/FVMF1JvZn+4w2FF6eFK1FoxknE1gyb tvoXnv8RfQliV0YjbEjA0OHfLNaB+dJqvwtn3FJdpEyqzhVNh/A3HHtOI9j4s3sf Rq8nWQKBgFfCtxwHq1nhBbXM8kCWNMv96ZGMOTpD8FJRU/XFwX+aJvGoUlxBaRvf Uha9h5L3RdtbVn7sLIgcvP8lo0iv5hVlAy6XroYRdL+rlUxVk/zOGYtmCrMTHTwP ySHyi59I/zX8VFa0zyGnF2CKEB+U6YKgjOHiN0sbysYofqR22dra -----END RSA PRIVATE KEY-----
Prawidłowo wykonanie żądanie powinno zwrócić nam kod 201 (Created) oraz w opis klucza w formacie JSON:
* About to connect() to evrshstore port 80 (#0) * Trying 192.168.1.1... connected > PUT /set_key?name=ssh_key HTTP/1.1 > User-Agent: curl/7.22.0 > Host: evrshstore > Accept: */* > X-Token: 0x6a6d3bc919eaa2a5d7e1118be3b9d7b50cb490af6ad3dd092e5c601cf04162fb > Content-Length: 1842 > Content-Type: application/x-www-form-urlencoded > Expect: 100-continue > < HTTP/1.1 100 Continue < HTTP/1.1 201 Created < Content-Type: application/json < Content-Length: 1857 < Accept-Ranges: bytes < Date: Sun, 28 Sep 2014 21:33:25 GMT < Connection: keep-alive < X-Cache: MISS < { "action":"set", "node":{ "key":"/0x6a6d3bc919eaa2a5d7e1118be3b9d7b50cb490af6ad3dd092e5c601cf04162fb/ssh_key", "value":"-----BEGIN RSA PRIVATE KEY-----\n-----END RSA PRIVATE KEY-----", "modifiedIndex":30, "createdIndex":30 } }
Po utworzeniu klucza ssh_key, który dostępny jest pod danym tokenem możemy teraz za pomocą metody get_key?name= (GET) pobrać jego wartość i wykorzystać w innej dowolnej usłudze. Ze względu na format JSON na wyjściu danych – wyciągnięcie czystego klucza wymaga małej obróbki za pomocą python’a:
curl -v -H 'X-Token: 0x6a6d3bc919eaa2a5d7e1118be3b9d7b50cb490af6ad3dd092e5c601cf04162fb'\ -L http://evrhstore/get_key?name=ssh_key | python -c 'import sys, json;\ print json.load(sys.stdin)["node"]["value"]' * About to connect() to evrshstore port 80 (#0) * Trying 192.168.1.1... connected > GET /get_key?name=ssh_key HTTP/1.1 > User-Agent: curl/7.22.0 > Host: evrhstore > Accept: */* > X-Token: 0x6a6d3bc919eaa2a5d7e1118be3b9d7b50cb490af6ad3dd092e5c601cf04162fb > < HTTP/1.1 200 OK < Content-Type: application/json < Content-Length: 1857 < Accept-Ranges: bytes < Date: Sun, 28 Sep 2014 21:55:51 GMT < Connection: keep-alive < X-Cache: MISS < { [data not shown] * Connection #0 to host evrshstore left intact * Closing connection #0 -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEApFYqFHx9zejQNV6Jx5JPz2WTnF22x0vdkDMPyMQqRWMior+B NfQTVdvGBZNW01C/NrCpldIGqg2Zh8UjVQBbzJwuewK5DmBcZfaBZLQ0B3q30ixr 6iLQ1lgtcfbNhpTELZ0+I0anrT5o4zQFKzHM4u5r806yCjr/lwN/WgA5fo0keGVH 0dmER/32vclGosuszQdNyBGI+PifxHX7Iy3SuI641OTptrCdQCQuYQZzocnzbFaw vRuOtHMqfq/zmVM1SI9LQw9qOobAMmpKTbGXqy1sUcDUNxANurLDqRCJW7BTjaKl SZelRckCLrN2Fr2U6SyuvFe2U2Snibc8H9OZhwIDAQABAoIBAQCJASCx1ZvYR4kV hUxealdJM4jdaq+P9Wqw5jD5krkfAegFQJzLS0G3abLsQQq4v2+6e4vWULOnoBDo RO0Q12yw52BEw19eYe2GP/1d5HIf7ipD+S66ku4CVJ8GjNiJo4rt8FK2fBgccZRm Hp9UhH8ojdbHkEsUBl3BG3RXpypHmrPVDdvKd5rJ8NMsxfwMPWOH/y5dU6yvpaVo AB4Fv7TRxpuWb47gI3fqsYT86+Vid5QRY/Cr+urBLeK2WK7T8trGBpiZBKwgVi4o vQJIfepbPDGRYzw339F2S0JzEk/U7rFhXu02OtJ/npqhaHTgI6H+31r6lAV/U8cg kvC0XqeZAoGBAM5CXLLX46TBI/Wyx87+apCcLikk7JQAXA9CLHI3OOQtp3mIGslT it4ilgDpdDjA31NtM5eolMdEG2kqY+jehPB/jkjo03tn14rdYRJSnKo2022lJYCS AdCE7kTUiAzzbxp7qD33Kc96uO+aTkeoTDvtCUXDoHSSSxfPUKxQnj/dAoGBAMv3 qXjp4fttGh/KegjhmP6qdf8z40ClB9aLV4CLcl0lQTYVXBnR1w3HbzVI+YnZuESK hTm4M4PLVOSTtVU9YcOOSK4Nphre/Kzxdtz9xmPzBntgE0DQNpuHK2YfRuFmwfWc /DbSa/65MSHP5BOdGlx5JR71zmEZNEHSOKNeqpqzAoGAZ2OmedNz/br+3oMuWxj2 q+RN0zv6Broja1adVudNcjtcTrQl0TM6Udz+WhirtGMhzvzXNrc/VJ9UKiQgjrMz 6iXWWb+zepFz3tzXcsrMUxpGYxi8MrV9iVuI4CG0zOEdmSXRELabU7BNkXVEtfCr vgI0eq7z+Fr4n0fBRY7ntFUCgYAZIm/T4p5iaVNqz3yyU3qTB1Z7GF3MvYl1ur4R rR1utQSQMZmj5OPnYsglfNSjVB0M9TTto/FVMF1JvZn+4w2FF6eFK1FoxknE1gyb tvoXnv8RfQliV0YjbEjA0OHfLNaB+dJqvwtn3FJdpEyqzhVNh/A3HHtOI9j4s3sf Rq8nWQKBgFfCtxwHq1nhBbXM8kCWNMv96ZGMOTpD8FJRU/XFwX+aJvGoUlxBaRvf Uha9h5L3RdtbVn7sLIgcvP8lo0iv5hVlAy6XroYRdL+rlUxVk/zOGYtmCrMTHTwP ySHyi59I/zX8VFa0zyGnF2CKEB+U6YKgjOHiN0sbysYofqR22dra -----END RSA PRIVATE KEY-----
Z czasem liczba kluczy może osiągnąć większą ilość, dlatego do ich wyświetlenia możemy wykorzystać metodę get_keys (GET), która wylistuje nam wszystkie klucze kryjące się pod danym tokenem:
curl -v -H 'X-Token: 0x6a6d3bc919eaa2a5d7e1118be3b9d7b50cb490af6ad3dd092e5c601cf04162fb'\ -L http://evrhstore/get_keys * About to connect() to evrshstore port 80 (#0) * Trying 192.168.1.1... connected > GET /get_keys HTTP/1.1 > User-Agent: curl/7.22.0 > Host: evrhstore > Accept: */* > X-Token: 0x6a6d3bc919eaa2a5d7e1118be3b9d7b50cb490af6ad3dd092e5c601cf04162fb > < HTTP/1.1 200 OK < Content-Type: application/json < Content-Length: 588 < Accept-Ranges: bytes < Date: Wed, 01 Oct 2014 19:26:55 GMT < Connection: keep-alive < X-Cache: MISS < * Connection #0 to host evrshstore left intact * Closing connection #0 { "action":"get", "node":{ "key":"/0x6a6d3bc919eaa2a5d7e1118be3b9d7b50cb490af6ad3dd092e5c601cf04162fb", "dir":true, "nodes":[ { "key":"/0x6a6d3bc919eaa2a5d7e1118be3b9d7b50cb490af6ad3dd092e5c601cf04162fb/29", "dir":true, "modifiedIndex":29, "createdIndex":29 }, { "key":"/0x6a6d3bc919eaa2a5d7e1118be3b9d7b50cb490af6ad3dd092e5c601cf04162fb/readme", "value":"Project for storing SSH Keys", "modifiedIndex":33, "createdIndex":33 }, { "key":"/0x6a6d3bc919eaa2a5d7e1118be3b9d7b50cb490af6ad3dd092e5c601cf04162fb/desc", "value":"SSH Keys", "modifiedIndex":31, "createdIndex":31 } ], "modifiedIndex":29, "createdIndex":29 } }
Podsumowując dostępne metody API:
- /get_token (GET) – generuje za każdym razem inny token dla użytkownika.
- /set_key?name={key} (PUT) – tworzy klucz o danej nazwie i wartości.
- /get_key?name={key} (GET) – pobiera wartość danego klucza.
- /get_keys (GET) – pobiera wszystkie klucze i ich wartości dla danego tokena.
- /del_key?name={key} (DELETE) – kasuje klucz o danej nazwie.
- /del_keys (DELETE) – kasuje wszystkie klucze wraz z tokenem.
Powyżej nie zostały przedstawione metody kasowania kluczy – są one jednak analogiczne do metod get_key oraz get_keys z tą różnicą, że używamy metody HTTP DELETE
, a nie GET
. Oprócz tych prostych metod sam backend w postaci etcd dodatkowo umożliwia nam: ustawianie kluczy o określonym czasie ważności (TTL) oraz nadpisywanie wartości klucza z możliwością odczytu poprzedniego stanu (prevNode). W odpowiedzi serwera za każdym razem widać nagłówek X-Cache: MISS
. Z myślą o wysokiej wydajności i ochronie serwerów backendowych dla metody /get_key na serwerze Varnish zostało ustawione przetrzymywanie odpowiedzi w pamięci cache na jedną minutę. Kolejne wywołanie tego samego klucza powinno zwrócić nagłówek X-Cache: HIT
. Całą konfigurację serwera Varnish dla usługi evrhstore możemy pobrać stąd. Oczywiście cała komunikacja z usługą powinna odbywać się za pomocą protokołu HTTPS, w przeciwnym wypadku wrażliwe dane wysyłane do usługi mogą zostać bardzo łatwo podsłuchane. W tym wypadku przed serwerem Varnish powinien znaleźć terminator SSL – na przykład serwer nginx pełniący rolę bramy. Dodatkowo w powyższym przykładzie został wykorzystany serwer w wersji 3.0.6, gdzie najnowszą wersją jest już 4.0.1, która posiada nieco inne podejście do logiki komunikacji z backendem, a w czasie pisania tego wpisu libvmody wykorzystane do stworzenia tej funkcjonalności nie zostały jeszcze przepisane na wyższą wersję.
2. Komunikacja z backendem:
Ze względu na architekturę etcd, w której tylko jeden serwer jest leaderem (tryb zapis / odczyt), a reszta pełni rolę tzw. “followers” (tryb tylko do odczytu z przekierowaniem do leadera) komunikacja z bazą została wykonana za pomocą serwera HAproxy. Spowodowane jest to również brakiem możliwości skorzystania z natywnego mechanizmu wyboru backendu w samym serwerze Varnish przez dodatek libvmod-curl. Jak nietrudno się domyślić na etapie wyboru kilku serwerów do odczytu i tylko jednego do zapisu – na poziomie protokołu HTTP należało rozdzielić serwery i metody służące do RW/RO i RO:
frontend etcdrouter bind :4002 mode http acl is_post method POST PUT use_backend etcd_l if is_post default_backend etcd_f backend etcd_l mode http balance roundrobin option httpclose option forwardfor option httpchk DELETE /v2/keys/httpcheck HTTP/1.1\r\n http-check expect status 404 server etcd1 192.168.1.1:4001 check server etcd2 192.168.1.2:4001 check backend etcd_f mode http balance roundrobin option httpclose option forwardfor server etcd1 192.168.1.1:4001 check server etcd2 192.168.1.2:4001 check
Ruta etcd_l służy do komunikacji z leaderem. Specjalnie przygotowany check w bardzo prosty sposób sprawdza, czy serwer z którym się komunikuje ma status leadera: próbuje skasować nieistniejący klucz – httpcheck
– jeśli serwer jest leaderem odpowie od razu kodem odpowiedzi 404 Not Found w przeciwnym wypadku kodem 301 wskazującym aktualny adres leadera. Pełną konfigurację serwera HAproxy można pobrać stąd. Dokumentacja również wspomina o tzw. spójnych odczytach, które powinny także odbywać się z leadera. Kwestia pozostaje do przetestowania przy ilu zapisach na sekundę występuje opóźnienie innych serwerów względem głównego.
3. Backend, architektura i uwagi końcowe:
Przy konstrukcji tego PoC zostały wykorzystane dwa serwery etcd połączone ze sobą. Komunikacja dla klienta i serwera została udostępniona na interfejsie zewnętrznym – serwer #1 został uruchomiony z parametrami:
./etcd -peer-addr 192.168.1.1:7001 -addr 192.168.1.1:4001 -name sshstore1
serwer #2:
./etcd -peer-addr 192.168.1.2:7001 -addr 192.168.1.2:4001 -name sshstore2 \ -peers 192.168.1.1:7001
Skalowanie usługi i zapewnienie wysokiej dostępności nie powinno stwarzać problemu dopóki nie dojdziemy do limitu wydajności ilości zapisów leadera. Jednak przeznaczenie usługi nie powinno opierać się głównie na bardzo dużej ilości zapisów – wręcz przeciwnie – powinna być głównie eksploatowana do odczytów przechowywanych informacji (zmiana kluczy SSH, czy konfiguracji nie występuje zbyt często nawet w ciągu dnia) – problematyczną kwestią może być ilość serwerów korzystająca z usługi (kilka zapisów razy setki serwerów wymaga już tysięcy zapisów w danym przedziale czasu). Każdy pojedynczy serwer usługi jest samowystarczalny tj. posiada własny serwer Varnish, HAproxy oraz etcd – monitorujący dostępność swojego backendu. Jak wcześniej zostało wspomniane – w celu zapewnienia bezpiecznej komunikacji do serwerów Varnish powinna ona zostać przepuszczona przez protokół HTTPS. Usługa terminująca SSL może jednocześnie pełnić rolę load balancera, który zgodnie z przyjętą polityką będzie rozkładać ruch do usługi. Jeśli nie ufamy infrastrukturze otaczającej serwery możemy dodatkowo zabezpieczyć komunikację między serwerami etcd za pomocą certyfikatów SSL – to samo tyczy się komunikacji dla klientów.
Usługa evrshstore jest typowym dowodem koncepcji. Stworzyłem ją w ciągu 10 godzin pracy mając z tyłu głowy coś RESTowego umożliwiające szybkie pobieranie kluczy SSH do serwerów. Do zadań utrzymania z pewnością wymagane jest wykonywanie kopii bezpieczeństwa oraz napisanie skryptu, którego zadaniem byłoby usuwanie pustych, nieużywanych tokenów. Nigdy nie została użyta produkcyjnie, ani przetestowana pod kątem prawdziwej wydajności.
Więcej informacji: etcd overview