Files
FSI/assignment2/relatorio/relatorio.tex
2026-05-11 11:38:51 +01:00

576 lines
21 KiB
TeX

\documentclass[11pt,a4paper]{article}
\usepackage[portuguese]{babel}
\usepackage[lining]{ebgaramond}
\usepackage{style}
\setlength{\parindent}{0em}
\setlength{\parskip}{2ex}
\title{Practical Assignment \#2}
\author{
João Neto -- 2023234004\\[1em]
Vasco Alves -- 2022228207
}
\begin{document}
\maketitle
\newpage
\tableofcontents
\newpage
\section{Introdução}
Este projeto tem como âmbito implementar, uma rede virtual privada (VPN) num cenário
de road-warrior, configurar \textit{two-factor authentication} (2FA) com os serviços
OpenVPN e Apache, e gerir certificados X.509 utilizando OCSP.
% NOTE(vasco): Eu acho que basta explicar o cenario e explicar como decidimos
% implementar <- yeah agree, also esta introdução acho que é boa fala sobre o objetivo
% e o cenario, e porque é que o nosso cenario é como é. Não sei se a parte das razões de
%segurança devia estar nesta parte ou na conclusão como perpetiva futura e reflexão, mas aqui
%também não está mal.
% Para tal, foi implementado um servidor e um cliente OpenVPN, certificados por uma
% autoridade central (CA) que em si é \textit{self-signed}. Para além disto, foi implementado
% um sistema de autenticação de dois factores através do plugin
% \textit{google-authenticator} para o OpenVPN e para o servidor de Apache.
Decidimos utilizar apenas três máquinas virtuais: o cliente (ou \textit{road warrior}),
a \textit{gateway} que utiliza OpenVPN e um servidor interno com OpenSSL e Apache.
Isto simplifica a elaboração do projecto, mas por razões de segurança poderia querer
separar a máquina de OpenSSL de outras máquinas destinadas a serviços da rede interna,
pois esta contém o \textit{certificate authority} (CA).
% Ambos o OpenVPN eo servidor Apache utilizam 2FA,
% recebendo o utilizador, e uma password que é uma concatenação da palavra-passe do utilizador
% e de uma password temporária (TOTP) de 6 dígitos. O servidor de Apache implementa a mesma autenticação.
\begin{tabular}{l l l}
{\bf Nome} & {\bf Script} & {\bf Rede} \\\toprule
Road Warrior & VM\_ROAD\_WARRIOR.sh & Rede Externa 193.168.0.0/24 \\
VPN Gateway & VM\_OPENVPN\_GATEWAY.sh & Router \\
OpenSSL / Apache & VM\_OPENSSL\_APACHE.sh & Rede Interna 10.60.0.0/24 \\
\end{tabular}
\section{Preparação Inicial}
\subsection{Criação de Certificados}
Os certificados utilizados foram auto-certificados por uma autoridade central que ``pertence''
à máquina de OpenSSL. Esta mesma faz a gestão da lista de revogação.
Todas as chaves foram criadas no mesmo computador, com as variáveis que estão
neste código. Aspetos importantes para mais tarde serão os parâmetros de Comon Name (CN)
pois servem para a validação do certificado ambos pelo OpenSSL e pelo browser.
Nós optamos por assumir que num cenário real, teríamos acesso físico às máquinas, por isso em vez
de utilizar, por exemplo SCP ou FTP, escolhemos partilhar os ficheiros a partir da máquina host. No entanto, outra abordagem também estaria correta.
\begin{codeblock}[bash]{create\_all\_keys.sh}
cert_ca="/C=PT/ST=Coimbra/L=Coimbra/O=UC/CN=CoimbraVPN"
cert_vpn="/C=PT/ST=Coimbra/L=Coimbra/O=UC/CN=gateway"
cert_user="/C=PT/ST=Coimbra/L=Coimbra/O=UC/CN=warrior"
cert_apache="/C=PT/ST=Coimbra/L=Coimbra/O=UC/CN=apache.coimbra"
openssl genrsa -out "ca.key" 2048
openssl req -x509 -nodes -days 365 -key "ca.key" -out "ca.crt" -subj "$cert_ca"
openssl genrsa -out "vpn.key" 2048
openssl req -new -key "vpn.key" -out "vpn.csr" -subj "$cert_vpn"
openssl ca -batch -in "vpn.csr" -cert "ca.crt" -keyfile "ca.key" -out "vpn.crt" -config cheese.cfg
openssl dhparam -out "dh2048.pem" 2048
openvpn --genkey secret "ta.key"
openssl genrsa -out user.key
openssl req -new -key user.key -out user.csr -subj "$cert_user"
openssl ca -batch -in "user.csr" -cert "ca.crt" -keyfile "ca.key" -out "user.crt" -config cheese.cfg
openssl genrsa -out apache.key
openssl req -new -key apache.key -out apache.csr -subj "$cert_apache" -addext "subjectAltName = IP:10.60.0.1,DNS:apache"
openssl ca -batch -in "apache.csr" -cert "ca.crt" -keyfile "ca.key" -out "apache.crt" -config cheese.cfg
openssl --genkey secret ta.key
\end{codeblock}
Como o CA foi criado ``\textit{in place}'', e não na sua pasta prédefinida, foi necessário utilizar
um configuração própria para definir os ficheiros \textit{index.txt} e \textit{serial}.
\begin{codeblock}[bash]{cheese.cfg}
[ ca ]
default_ca = CA_default
[ CA_default ]
default_days = 365
database = index.txt
serial = serial
copy_extensions = copy
new_certs_dir = .
default_md = sha256
policy = policy_any
[ policy_any ]
commonName = supplied
\end{codeblock}
\subsection{Configuração geral}
Para evitar repetição e redundancia; e para garantir consistencia na elaboração do projeto criamos varios shell scripts, um destinado a cada maquina virtual.
Para configurar as VMs era preciso introduzir os mesmos comandos várias vezes, o que levava muitas vezes a erros de escrita,
ou a correr o mesmo comando várias vezes, por isso criamos vários ficheiros .sh para conseguir facilitar o processo.
A utilização de ficheiros .sh também vem com outros positivos pois facilita a testagem, e a recriação do cenário rapidamente.
No entanto para os serviços que configuramos, instalar, desativar e dar flush às iptables não foi suficiente, tivemos que criar
pastas e sincronizar os relógios de todas as VMs visto que elas estarem ligeiramente atrasadas nunca conseguíamos acertar na
password do google-authenticator visto que utiliza o tempo local para calcular a sua chave.
\begin{codeblock}[bash]{VM\_CONFIG.sh}
yum install -y epel-release
yum install -y openvpn iptables-services dhcp-client
systemctl stop firewalld
systemctl disable firewalld
systemctl mask firewalld
systemctl enable iptables
iptables -F
CA_DIR="/etc/pki/CA"
mkdir -p "${CA_DIR}/newcerts"
mkdir -p "${CA_DIR}/private"
touch "${CA_DIR}/index.txt"
cp ca/serial "${CA_DIR}/serial"
mkdir -p /etc/openvpn/server
mkdir -p /etc/openvpn/client
# NOTE(vasco): tive problemas com a sincronizacao de tempo
# se nao tiver sincronizado, o TOTP nao funciona
systemctl stop chronyd
ntpdate pool.ntp.org
systemctl start chronyd
\end{codeblock}
\section{VPN Gateway}
\subsection{Configuração da Máquina}
Como já foi dito anteriormente, cada máquina vem com um \textit{script}
que instala toda a configuração necessária.
Para que a gateway funcione como router entre a rede externa e a rede interna,
foi necessário ativar o \textit{IP forwarding} no kernel e configurar as regras
de \textit{iptables} para permitir o tráfego da VPN e realizar o mascaramento
de IP (NAT).
% NOTA(vasco): Não temos regras de DROP a packets
% talvez deviamos mudar isso nao sei <- não diz nada no enunciado ¯\_(ツ)_/¯
% também o trabalho não é sobre ip tables por isso it does make sense não fazer drop
%e utilizar as regras apenas para encaminhar corretamente.
% Colocar isso na conclusão tho
\begin{codeblock}[bash]{VM\_VPN\_GATEWAY.sh}
#!/bin/bash
# --- configuracao --- #
source VM_CONFIG.sh
yum install -y google-authenticator qrencode ntpsec
# --- forwarding --- #
if_fora="enp0s8"
ip_fora="193.136.212.1"
if_dentro="enp0s9"
ip_dentro="10.60.0.3"
mega_tunel="tun0"
ip_mega_tunel="10.8.0.0/24"
ifconfig $if_fora $ip_fora netmask 255.255.255.0
ifconfig $if_dentro $ip_dentro netmask 255.255.255.0
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
sysctl -p /etc/sysctl.conf
iptables -I INPUT 1 -p udp --dport 1194 -j ACCEPT
iptables -I FORWARD 1 -i $mega_tunel -o $if_dentro -j ACCEPT
iptables -I FORWARD 1 -i $if_dentro -o $mega_tunel -j ACCEPT
iptables -I FORWARD 1 -i $mega_tunel -o $if_fora -j ACCEPT
iptables -I FORWARD 1 -i $if_fora -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -t nat -A POSTROUTING -s $ip_mega_tunel -o $if_fora -j MASQUERADE
iptables-save > /etc/sysconfig/iptables
# --- vpn server --- #
vpn_dir="/etc/openvpn/server"
cp ca/ta.key $vpn_dir
cp ca/ca.crt $vpn_dir
cp ca/vpn.key $vpn_dir
cp ca/vpn.crt $vpn_dir
cp ca/dh2048.pem $vpn_dir
cp conf/vpn.conf $vpn_dir
cp conf/ocsp-verify.sh $vpn_dir
cp conf/totp /etc/pam.d/
# --- utilizador --- #
id -u john &>/dev/null || useradd john
echo "password" | passwd --stdin john
openvpn --config /etc/openvpn/server/vpn.conf
\end{codeblock}
\subsection{Configuração do Serviço OpenVPN}
O servidor OpenVPN utiliza um certificado X.509 assinado pelo nosso \textit{Certificate Authority} (CA).
E faz uso de um script \texttt{oscp-verify.sh} para validar ou revogar os certificados através do servidor OCSP.
\begin{codeblock}{vpn.conf}
local 193.136.212.1
port 1194
proto udp
dev tun
verb 4
ca /etc/openvpn/server/ca.crt
cert /etc/openvpn/server/vpn.crt
key /etc/openvpn/server/vpn.key
dh /etc/openvpn/server/dh2048.pem
topology subnet
server 10.8.0.0 255.255.255.0
push "route 10.60.0.0 255.255.255.0"
# ocsp and revocation
script-security 2
tls-verify /etc/openvpn/server/ocsp-verify.sh
# auth
cipher AES-256-GCM
auth SHA256
plugin /usr/lib64/openvpn/plugins/openvpn-plugin-auth-pam.so totp
tls-auth /etc/openvpn/server/ta.key 0
\end{codeblock}
Foi criado o ficheiro \texttt{totp} com a configuração de autenticação a
ser utilizada pelo plugin de PAM para o openvpn.
\begin{codeblock}{totp}
auth required pam_google_authenticator.so forward_pass
auth required pam_unix.so use_first_pass
account required pam_unix.so
\end{codeblock}
Este script simplesmente comunica com o servidor OpenSSl
e verifica o resultado.
\begin{codeblock}{ocsp\_verify.sh}
#!/bin/bash
depth=$1
if [ "$depth" -eq 0 ]; then
if [ -n "$tls_serial_0" ]; then
# e preciso converter o serial para hexadecimal porque o openssl espera em hex
hex_serial=$(printf '%x' "$tls_serial_0")
status=$(openssl ocsp -issuer /etc/openvpn/server/ca.crt -serial "0x$hex_serial" -url http://10.60.0.1:8888 -CAfile /etc/openvpn/server/ca.crt 2>/dev/null)
if echo "$status" | grep -q "good"; then
exit 0 # sucesso
fi
exit 1 # revogado ou nao encontrado
fi
exit 1
fi
\end{codeblock}
\subsection{Erros}
Um dos erros que encontramos pelo caminho foi que o OpenSSL OCSP espera que o
\textit{serial} esteja num formato diferente do que o esperado. Foi necessário
converter para hexadecimal primeiro.
Adicionalmente, devido às restrições de segurança do \textit{systemd},
tentamos desativar o \texttt{ProtectHome} no serviço do OpenVPN
para que o plugin PAM consiga ler os ficheiros de segredo do Google Authenticator
localizados nas diretorias \textit{home} dos utilizadores. Mas isto não
foi suficiente, por isso acabamos por correr os serviços pela linha
de comandoos.
\subsection{Configurar o utilizador com TOTP}
Primeiro, na gateway, entramos como o utilizador desejado e obtemos a chave
do gerador de palavras passes temporárias. Ao inserir a chave no
\texttt{google authenticator} podemos obter um código QR, a nossa primeira
chave de 6 dígitos.
\begin{figure}[h]
\centering
\includegraphics[width=8em]{google-authenticator}
\end{figure}
\begin{codeblock}[bash]{}
su john
google-authenticator
\end{codeblock}
\section{VPN Client (Road Warrior)}
\subsection{Configuração da Máquina}
Para a configuração da Máquina, configuramos o edereço, o default gateway e adicionamos apache aos Hosts:
\begin{codeblock}{VM\_ROAD\_WARRIOR.sh}
#!/bin/bash
# --- configuracao --- #
source VM_CONFIG.sh
ifconfig enp0s8 193.136.212.10 netmask 255.255.255.0
route add default gw 193.136.212.1
if ! grep -q "apache" /etc/hosts; then
echo "10.60.0.1 apache" >> /etc/hosts
fi
# --- vpn client --- #
vpn_dir="/etc/openvpn/client/"
cp ca/ta.key $vpn_dir
cp ca/ca.crt $vpn_dir
cp ca/user.key $vpn_dir
cp ca/user.crt $vpn_dir
cp conf/client.conf $vpn_dir
openvpn --config "${vpn_dir}/client.conf"
\end{codeblock}
% Esta configuração foi necessaria, porque sem edereço a VM não conseguia-se identificar na rede. Sem o default gateway
% os edereços desconhecidos seriam enviados para a porta da internet, e adicionamos apache aos Hosts para que fosse igual
% ao domain para não haver erros.
%(I dunno about this Apache part??) Also sinto que ainda precisa de mais um bocado.
Também foram movidos os certificados e chaves necessarias para as pastas do serviço openvpn, para que o Road Warrior
consiga comunicar e ser validado pela gateway.
\subsection{Configuração do Cliente OpenVPN}
O cliente encontra-se na rede externa (\texttt{193.136.212.10}) e liga-se à VPN
gateway na porta 1194. Para garantir a segurança, utilizamos autenticação mútua (os certificados X.509)
e um \textit{two factor authentication} (2FA) como palavras-passe temporárias, geradas através do
\textit{Google Authenticator}.
\begin{codeblock}{client.conf}
client
dev tun
proto udp
remote 193.136.212.1 1194
ca ca.crt
cert user.crt
key user.key
auth-user-pass
cipher AES-256-GCM
auth SHA256
\end{codeblock}
\subsection{Testes}
Para verificar que a autenticação foi corretamente implementada, inserimos a password de um utilizador sem os digitos do TOTP, e identificamos que utilizar somente a password não é suficiente para autenticar. Igualmente ao utilizar ambos a autenticação é bem sucedida.
Para verificar que o tunel foi estabelecido, primeiro corremos na linha de comandos \texttt{ip a}. Observamos a existencia de uma nova interface tun0, ou seja o tunel foi corretamente establecido. Depois demos ping ao route e depois ao servidor interno, que resultou em pacotes devolvidos para ambos.
% TODO: screenshots? dizer que erros exatos nos obtemos a cada etapa
% TODO: erros ortograficos lol
Para verificar que o OCSP funciona correctamente, o cliente conectou ao servidor OpenVPN:
primeiro, sem o servidor OCSP a correr, uma segunda vez com ele a correr e com o certificado correcto
e uma terceira vez com um certificado revogado. Fizemos estes testes sabendo que o
cliente e o servidor já estavam correctamente configurados.
Verificamos que, como é suposto: sem OCSP não é possivel autenticar; com OCSP e com certificado válido,
podemos autenticar; e com OCSP mas com certificado revogado, a autenticação falha.
\section{Servidor Apache e OCSP}
Para a configuração da ultima maquina, temos o OpenSSL e Apache no mesmo servidor, por isso temos de configurar
as pastas necessarias, os utilizadores do serviço, configurar os edereços e uma route:
\begin{codeblock}{VM\_OPENSSL\_APACHE.sh}
#!/bin/bash
# configuracao
source VM_CONFIG.sh
sudo yum install -y epel-release
sudo yum install -y openssl httpd mod_ssl mod_authnz_pam google-authenticator
sudo yum install -y mod_session
# utilizador
id -u john &>/dev/null || useradd john
echo "password" | passwd --stdin john
if_dentro="enp0s8"
ip_dentro="10.60.0.1"
ifconfig $if_dentro $ip_dentro netmask 255.255.255.0
# route de volta para comunicar com o warrior
route add -net 10.8.0.0 netmask 255.255.255.0 gw 10.60.0.3
cp conf/openssl.cnf /etc/pki/tls/
# copiar ca para esta VM
cp ca/index.txt $CA_DIR
cp ca/ca.crt $CA_DIR
cp ca/ca.key $CA_DIR
cp ca/serial $CA_DIR
cp ca/dh2048.pem $CA_DIR
# correr oscp
killall openssl 2>/dev/null
openssl ocsp -index $CA_DIR/index.txt -port 8888 -rsigner $CA_DIR/ca.crt -rkey $CA_DIR/ca.key -CA $CA_DIR/ca.crt -text &
# apache
mkdir -p /etc/httpd/ssl
cp ca/ca.crt /etc/httpd/ssl/
cp ca/apache.crt /etc/httpd/ssl/
cp ca/apache.key /etc/httpd/ssl/
cp conf/ssl.conf /etc/httpd/conf.d/ssl.conf
cp conf/httpd.conf /etc/httpd/conf/httpd.conf
cp conf/httpd-totp /etc/pam.d/httpd-totp
echo "LoadModule session_module modules/mod_session.so" > /etc/httpd/conf.modules.d/01-session.conf
echo "LoadModule session_cookie_module modules/mod_session_cookie.so" >> /etc/httpd/conf.modules.d/01-session.conf
echo "LoadModule auth_form_module modules/mod_auth_form.so" > /etc/httpd/conf.modules.d/01-auth_form.conf
cp -r www/* /var/www/html/
chown -R apache:apache /var/www/html/
httpd -X
\end{codeblock}
\subsection{Configuração da Máquina}
Como já referimos a Máquina tem ambos o serviço OpenSSL e Apache, por isso vai precisar de dois .conf files para
configurar-los. O httpd.conf tem as portas e modulos enquanto o ssl.conf tem a configuração da autenticação mútua, e o OCSP:
\begin{codeblock}{httpd.conf}
ServerRoot "/etc/httpd"
Include conf.modules.d/*.conf
LoadModule authnz_pam_module modules/mod_authnz_pam.so
LoadModule mpm_event_module modules/mod_mpm_event.so
User apache
Group apache
Listen 80
Listen 443
Include conf.d/*.conf
DocumentRoot "/var/www/html"
<Directory "/var/www/html">
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
\end{codeblock}
\begin{codeblock}{ssl.conf}
<VirtualHost *:443>
ServerName 10.60.0.1
DocumentRoot /var/www/html
SSLEngine on
SSLCertificateFile /etc/httpd/ssl/apache.crt
SSLCertificateKeyFile /etc/httpd/ssl/apache.key
SSLCACertificateFile /etc/httpd/ssl/ca.crt
# mutual authentication
SSLVerifyClient require
SSLVerifyDepth 1
# ocsp validation
SSLOCSPEnable on
SSLOCSPDefaultResponder "http://10.60.0.1:8888"
SSLOCSPOverrideResponder on
SSLOCSPUseRequestNonce off
# session management
Session On
SessionCookieName session path=/;HttpOnly;Secure
# proteger
<Location "/">
AuthType Form
AuthName "Coimbra VPN"
AuthFormProvider PAM
AuthPAMService httpd-totp
AuthFormLoginRequiredLocation "/login.html"
Require valid-user
</Location>
# public login page
<Location "/login.html">
AuthType None
Require all granted
</Location>
# login handler
<Location "/dologin">
SetHandler form-login-handler
AuthType Form
AuthName "Coimbra VPN"
AuthFormProvider PAM
AuthPAMService httpd-totp
Require all granted
AuthFormLoginSuccessLocation "/index.html"
AuthFormLoginRequiredLocation "/login.html?error=1"
</Location>
# logout handler
<Location "/logout">
SetHandler form-logout-handler
AuthFormLogoutLocation "/login.html?loggedout=1"
</Location>
</VirtualHost>
# redirect para https
<VirtualHost *:80>
ServerName 10.60.0.1
Redirect permanent / https://10.60.0.1/
</VirtualHost>
\end{codeblock}
\subsubsection{Testes}
\begin{itemize}
\item \textbf{Domínio:} Verificou-se que o acesso só é permitido utilizando o endereço correto, pois se for inserido outro dominio, não é direcionado para o site do Apache.
\item \textbf{Redirecionamento HTTPS:} Ao testar quando colocamos http, e o dominio certo, era redirecionado para https.
\item \textbf{Autenticação com o Certificado:} O acesso foi negado ao apresentar certificados inválidos ou ausentes no browser, devolvendo um erro com sobre não conseguir establecer connexão porque falta de certificado.
\end{itemize}
Para testar o OCSP, fizemos os seguintes paços:
\begin{enumerate}
\item Estabelecer a ligação VPN e verificar a conectividade à rede interna.
\item No diretório da autoridade de certificação (máquina \textit{host}), revogar o certificado do utilizador:
\begin{codeblock}[bash]{revoke.sh}
openssl ca -revoke user.crt -config cheese.cfg -keyfile ca.key -cert ca.crt
\end{codeblock}
\item Atualizar o ficheiro \texttt{index.txt} no servidor OCSP e reiniciar o serviço para carregar o novo estado de revogação.
\item Tentar estabelecer uma nova ligação VPN e verificar que a autenticação falha devido à resposta \texttt{revoked} do responder OCSP.
\end{enumerate}
\section{Teste Integrado}
Para validar, efetuámos um teste integrado englobando todos os requisitos:
\begin{enumerate}
\item Começamos por iniciar todas as máquinas com os devidos \textit{scripts}.
\item Na máquina \textit{Road Warrior}, iniciámos a ligação OpenVPN com o utilizador, a sua password e o \textit{token} TOTP.
\item O \textit{Gateway} OpenVPN verifica as credenciais e verifica o certificado cliente contra o servidor OCSP.
\item Antes de acedermos ao firefox, temos que verificar que já adicionámos a nossa a nossa CA e o certificado \texttt{p12}.
\item Através do túnel VPN, acedemos agora ao endereço \texttt{https://apache.coimbra} no browser.
\item O servidor Apache solicitou o certificado X.509 do utilizador e validou a sua autenticidade e estado de revogação no OCSP.
\item Finalmente, o Apache apresentou a página de login, onde inserimos as credenciais e o código TOTP.
\end{enumerate}
\section{Conclusão}
Atingimos o objetivo deste trabalho: conseguimos configurar o túnel VPN,
o \textit{two-factor authentication} em múltiplos serviços, e conseguimos gerir o ciclo de vida dos
certificados emitidos através de uma CA própria e OCSP. Utilizar mais máquinas para simular um cenário
maior seria redundante e apenas exigiria a emissão de mais certificados, não acrescentando muito ao nível de aprendizagem.
Aplicando conhecimentos de trabalhos anteriores,
poderíamos aplicar políticas mais restritas nas \textit{iptables} (ex: regras de DROP aos pacotes indesejados),
e implementar ferramentas como o Suricata para identificar possíveis anomalias e ataques aos serviços.
\end{document}