NGINX: Nieodpowiednie użycie dyrektywy deny

21 May 2016

allow  best-practices  deny  http  nginx 

Share on:

Dyrektywy allow oraz deny dostarczane są z modułem ngx_http_access_module i umożliwiają zezwolenie na dostęp lub jego ograniczenie do wybranych adresów klientów. Obie nadają się do budowania list kontroli dostępu.

Moim zdaniem najlepszym sposobem budowania list ACL jest zacząć zawsze od odmowy, a następnie przyznawać dostęp adresom IP tylko do wybranych lokalizacji. NGINX dostarcza podobny mechanizm nadawania dostępu, jednak reguły są przetwarzane w kolejności, od góry do dołu: jeśli pierwszą dyrektywą w sekwencji będzie deny all, wówczas wszystkie dalsze dyrektywy allow nie przyniosą żadnego efektu. Dlatego regułę blokującą zawsze ustawiamy po dyrektywach zezwalających:

location /login {

  allow 192.168.252.10;
  allow 192.168.252.11;
  allow 192.168.252.12;
  deny all;

}

Pamiętaj, że dyrektywa deny zawsze zwróci kod błędu 403 Forbidden, odnoszący się do klienta uzyskującego dostęp, który nie jest upoważniony do wykonania danego żądania. Został on zdefiniowany w RFC 7231 [IETF] i dokładnie oznacza, że serwer, który przyjął żądanie, w pełni je rozumie jednak odmawia autoryzacji.

Czy stosowanie powyższych dyrektyw może rodzić jakieś negatywne konsekwencje? Zdecydowanie tak: obie dyrektywy mogą działać wbrew oczekiwaniom, zwłaszcza, łącząc je z mechanizmem przepisywania dostarczanym przez serwer NGINX. Taka konfiguracja może być jednak dosyć specyficzna i możliwe, że nigdy z niej nie skorzystasz, chyba że zamiast odpowiedzi bezpośrednio do klienta przekierujesz ruch w inne miejsce. Więcej na ten temat poczytasz na blogu OpenResty w rozdziale Nginx directive execution order (03), w którym niezwykle dokładnie opisano cały przypadek.

Wracając do problemu, spójrz na poniższy przykład:

server {

  server_name example.com;

  deny all;

  location = /test {
    return 200 "it's all okay";
    more_set_headers 'Content-Type: text/plain';
  }

}

Następnie, wykonując poniższe żądanie:

curl -i https://example.com/test
HTTP/2 200
date: Wed, 11 Nov 2018 10:02:45 GMT
content-length: 13
server: Unknown
content-type: text/plain

it's all okay

Widzisz, że dostaliśmy odpowiedź o treści “it’s all okay” z kodem 200. Dlaczego, skoro jawnie zablokowaliśmy dostęp do całego zasobu /test za pomocą dyrektywy deny i która jest jakby nad kontekstem location = /test w konfiguracji (tj. jej zakres rozchodzi się na cały blok server)?

Jest to prawidłowe zachowanie i ma związek z całym mechanizmem przetwarzania żądań. Każdy request, jak już dotrze do serwera NGINX, zostaje przetwarzany w tzw. fazach. Tych faz jest dokładnie jedenaście:

Przygotowałem również proste wyjaśnienie, które pomoże ci zrozumieć, jakie moduły oraz dyrektywy są używane na każdym etapie:

Dodatkowo każda z faz ma listę powiązanych z nią procedur obsługi. Co więcej, na każdej fazie można zarejestrować dowolną liczbę handlerów.

Polecam zapoznać się ze świetnym wyjaśnieniem dotyczącym faz przetwarzania żądań. Dodatkowo, w tym oficjalnym przewodniku także dość dokładnie opisano cały proces przejścia żądania przez każdą z faz.

Wróćmy teraz do naszego problemu i „dziwnego” zachowania dyrektywy deny w połączeniu z wykorzystaniem dyrektywy return — co w konsekwencji prowadzi do natychmiastowego przesłania odpowiedzi do klienta, a nie zablokowania dostępu do danego zasobu.

Jak już wspomniałem, wynika to z faktu, że przetwarzanie żądania odbywa się w fazach, a faza przepisywania (do której należy dyrektywa return) wykonywana jest przed fazą dostępu (w której działa dyrektywa deny). Niestety NGINX nie zgłasza nic niepokojącego (bo i po co) podczas przeładowania, więc odpowiedzialność poprawnego budowania reguł filtrujących wraz z pozostałymi mechanizmami spada na administratora.

Jednym z rozwiązań jest użycie instrukcji if w połączeniu z modułami geo lub map. Na przykład:

  server_name example.com;

  location / {

    if ($whitelist.acl) {

      set $pass 1;

    }

    if ($pass = 1) {

      return 200 "it's all okay";
      # lub:
      # proxy_pass http://bk_web01;

      more_set_headers 'Content-Type: text/plain';

    }

    if ($pass != 1) {

      return 403;

    }

  }

Zgodnie z dokumentacją, nie zaleca się używania instrukcji if (zwłaszcza w kontekście lokalizacji), chociaż moim zdaniem, użycie takiej konstrukcji może być nieco bardziej elastyczne oraz bezpieczniejsze dzięki wykorzystaniu ww. modułów. Po drugie, sama dokumentacja wskazuje przypadki użycia, w których po prostu nie można uniknąć użycia tego warunku, na przykład jeśli trzeba przetestować zmienną, która nie ma równoważnej dyrektywy.

Planując budowanie list kontroli dostępu, rozważ kilka opcji, z których możesz skorzystać. NGINX dostarcza moduły ngx_http_access_module, ngx_http_geo_module, ngx_http_map_module lub ngx_http_auth_basic_module, które pozwalają na nadawanie dostępów i zabezpieczanie miejsc w aplikacji.

Zawsze powinieneś przetestować swoje reguły przed ich ostatecznym wdrożeniem: