19 stycznia 2015 blog security ssh apache lightdm cgi

Port knocking z uwierzytelnianiem.

Pomysł

Port knocking jest jedną z metod zabezpieczeń. Działa to w ten sposób, że klient otwiera połączenia do serwera na wskazane porty w ścisłej kolejności. Po przechwyceniu takiej sekwencji, serwer otwiera jakiś inny port, dzięki czemu jest możliwe połączenie np. SSH.

Zaprezentowany tu pomysł jest uproszczeniem wspomnianej idei. Klient otwiera stronę i po uwierzytelnieniu, czyli podaniu nazwy użytkownika i hasła, serwer otwiera port (lub porty) dla klienta. Największymi zaletami są: dodatkowe sprawdzenie uprawnień (bo klient musi się zalogować) oraz uniezależnienie się od dodatkowego oprogramowania używanego do standardowej techniki port knocking.

Trzeba pamiętać, że uwierzytelnianie podstawowe (HTTP Basic) jest bardzo łatwe do podsłuchania i użycia przez innych, dlatego zalecane jest włączenie szyfrowania (TLS).

Wymagania

Opis działania

Klient wchodzi na stronę allow.php, wpisuje nazwę użytkownika i hasło. Jego adres IP jest pobierany i zapisywany do katalogu good-ip.

Dzięki skryptowi dirwatch.sh uruchomionemu jako root (tylko on może wykonywać iptables) plik utworzony przez allow.php zostaje "zauważony".

Zostaje uruchomiony skrypt allow.sh z jednym parametrem - nazwą pliku, w którym został zapisany adres IP. Adres IP zostaje wczytany i reguła dopuszczająca IP klienta jest dodawana do łańcucha reguł GOOD.

Reguły iptables są konfigurowane dzięki /etc/network/if-pre-up.d/firewall, który wczytuje reguły firewalla przy nawiązaniu połączenia sieciowego z plików /etc/ip6tables.firewall.rules oraz /etc/iptables.firewall.rules.

Dodatkowo dzięki crontab skrypt monitorujący dirwatch.sh jest uruchamiany przy starcie systemu.

Konfiguracja

Aby przetestować działanie port knocking wystarczy uruchomić (pamiętając o zamianie nazw katalogów na właściwe):

/root/dirwatch.sh /var/www/html/portknocking/good-ip/ /root/allow.sh

Po wejściu na stronę allow.php w konsoli powinny wyświetlić się informacje o próbie dodania adresu klienta do łańcucha reguł. Można też skorzystać z programu curl:

curl -u user:password https://devsite.pl/portknocking/allow.php

allow.sh

Główna część konfiguracji. Skrypt przyjmuje adres IP jako parametr, sprawdza czy już wcześniej nie był dodany do łańcucha GOOD reguł iptables i go dodaje.

#! /usr/bin/env bash

REGEXP_IP='([0-9]{1,3}[\.]){3}[0-9]{1,3}'

listGood() {
	iptables -nL GOOD | \
		awk '{print($4);}' | \
		grep -E "$REGEXP_IP" | \
		sort -t . -k 1,1n -k 2,2n -k 3,3n -k 4,4n
}

help() {
cat <<EOF
Allowed IPs manager.

Parameters
	-l, --list	list good ips
	-r, --reset	reset firewall
	-h, --help	show this help
EOF
}

if [[ "$1" == '-h' || "$1" == '--help' ]]; then
	help
	exit 0
fi

if [ "$EUID" -ne 0 ]; then
	echo "Please run as root."
	echo "Run `basename $0` --help for help."
	exit 3
fi

if [[ "$1" == '-l' || "$1" == '--list' ]]; then
	listGood
	exit 0
elif [[ "$1" == '-r' || "$1" == '--reset' ]]; then
	/etc/network/if-pre-up.d/firewall
	RESULT=$?
	exit $RESULT
elif [[ "$1" != "" ]]; then
	echo "Checking $1 on `date`"
	if [ -f "$1" ]; then
		echo "It appears to be a file"
		IP=`head -n 1 "$1" | grep -E "$REGEXP_IP"`
	else
		IP=`echo "$1" | grep -E "$REGEXP_IP"`
	fi
	if [[ "$IP" != "" ]]; then
		echo "$IP appears to be an IP"
		found=`listGood | grep -F "$IP"`
		if [[ "$found" == "" ]]; then
			echo "Addig $IP to good-list"
			iptables -A GOOD -s "$IP" -j ACCEPT
			RESULT=$?
			exit $RESULT
		else
			echo "$IP is already good-listed"
			exit 0
		fi
	else
		echo "$1 is not an IP"
		echo "Run `basename $0` --help for help."
		exit 2
	fi
else
	echo "Run `basename $0` --help for help."
	exit 1
fi

dirwatch.sh

Skrypt ten jest odpowiedzialny za monitorowanie katalogu (przy użyciu inotifywait). Jeśli plik zostanie zamknięty po zapisaniu close_write uruchamiany jest skrypt podany jako parametr.

Aktualna wersja skryptu znajduje się w repozytorium.

#! /usr/bin/env bash

help() {
cat <<EOF
Watch given dir for created files using inotify-tools.

Usage:
	`basename $0` directory commands and parameters

The script executes [commands and parameters] [created file]
for every created file and DELETES the file.
EOF
}

fail() {
	echo $*
	echo "Run `basename $0` --help for help."
	exit 1
}

if [[ "$1" == '-h' || "$1" == '--help' ]]; then
	help
	exit 0
fi

if [[ "$1" == "" ]]; then
	fail "This script needs at least two parameters."
fi

pushd "$1" >/dev/null || fail "Cannot cd into the directory $1"
DIR="`pwd`/"
popd >/dev/null
shift

if [ ! -d "$DIR" ]; then
	fail "Directory $DIR does not exist."
fi

if [[ "$1" == "" ]]; then
	fail "This script needs at least two parameters."
fi

inotifywait -m -e close_write -r "$DIR" | \
	while read l; do \
		l=${l/*CLOSE /}; \
		[ -f "${DIR}${l}" ] && \
			$* "${DIR}${l}" && rm -f "${DIR}${l}" || \
			echo "Given command returned non-zero for ${DIR}${l}."
	done

/etc/network/if-pre-up.d/firewall

W Ubuntu jest to plik, który jest uruchamiany przy nawiązaniu połączenia sieciowego. Musi być wykonywalny.

#! /usr/bin/env sh
/sbin/iptables-restore < /etc/iptables.firewall.rules
/sbin/ip6tables-restore < /etc/ip6tables.firewall.rules

/etc/iptables.firewall.rules

Jest to przykładowa konfiguracja. Tworzony jest łańcuch reguł GOOD, do którego dodawane są numery IP autoryzowanych klientów.

Dla każdego portu (w przykładzie 22), który ma być chroniony, są dodane dwie linie:

-A INPUT -p tcp --dport 22 -j GOOD
-A INPUT -p tcp --dport 22 -j DROP

Pierwsza z nich powoduje przeskoczenie filtrowania do łańcucha GOOD, a druga zabrania dostępu do portu.

*filter

-A INPUT -i lo -j ACCEPT
-A INPUT -d 127.0.0.0/8 -j REJECT
-N GOOD

-A OUTPUT -j ACCEPT

-A INPUT -p tcp --dport 443 -j ACCEPT

-A INPUT -p tcp --dport 22 -j GOOD
-A INPUT -p tcp --dport 22 -j DROP

-A INPUT -p icmp --icmp-type echo-request -j ACCEPT

-A INPUT -m limit --limit 5/min -j LOG --log-prefix "iptables denied: " --log-level 7

-A INPUT -j DROP
-A FORWARD -j DROP

COMMIT

/etc/ip6tables.firewall.rules

Reguły dla IPv6. Jeśli w systemie IPv6 jest wyłączony, nie jest konieczne ich dodawanie.

-P INPUT DROP
-P FORWARD DROP
-P OUTPUT ACCEPT

-A INPUT	-m state --state ESTABLISHED,RELATED -j ACCEPT
-A OUTPUT -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
-A INPUT	-p ipv6-icmp -j ACCEPT
-A OUTPUT -p ipv6-icmp -j ACCEPT

COMMIT

crontab

Pierwszy wpis resetuje otwarte porty codziennie, o piątej rano. Druga linia uruchamia skrypt monitorujący katalog good-ip.

0 5 * * *	/etc/network/if-pre-up.d/firewall
@reboot	dtach -n /tmp/dtach.allow.ip /root/dirwatch.sh /var/www/html/portknocking/good-ip/ /root/allow.sh &

allow.php

Należy utworzyć katalog good-ip i umożliwić zapisywanie do niego innym użytkownikom (np. chmod 777 good-ip), ponieważ domyślnie Apache jest uruchomiany jako użytkownik apache.

<?php
@set_time_limit(0);
@error_reporting(E_ALL ^ E_NOTICE);

header("Expires: Tue, 03 Jul 2001 06:00:00 GMT");
// header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");

if($_SERVER['HTTP_CLIENT_IP']) {
		$ip = $_SERVER['HTTP_CLIENT_IP'];
} else if($_SERVER['HTTP_X_FORWARDED_FOR']) {
		$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else if($_SERVER['HTTP_VIA']) {
		$ip = $_SERVER['HTTP_VIA'];
} else if($_SERVER['REMOTE_ADDR']) {
		$ip = $_SERVER['REMOTE_ADDR'];
} else {
		$ip = "?";
}
echo $ip;
$f = "good-ip/" . $ip . ".ip";
$fh = fopen($f, 'w') or die("can't open file");
fwrite($fh, $ip);
fclose($fh);
?>

VirtualHost

Utworzenie pliku z hasłem:

htpasswd -c /etc/htpasswd username

Konfiguracja aplikacji (w Ubuntu /etc/apache2/sites-enabled/domena.conf):

Alias /portknocking /var/www/html/portknocking
<Directory "/var/www/html/portknocking">
	AuthUserFile /etc/htpasswd
	AuthType Basic
	AuthName "devsite.pl http port knocking authentication"
	Require valid-user
</Directory>

Linki