Vamos subir o servidor web, do tipo bem seguro. Ele será útil para as tarefas administrativas e acesso ao serviço de agenda, mas não terá qualquer utilidade para o email. Lembre-se: esta série não fornece um webmail.

Já abandonei o Apache há algum tempo em favor do nginx. Por isso é ele que vamos utilizar aqui.

A configuração do nginx para os nossos casos de uso é bastante complexa. Muito dessa complexidade é motivada pela necessidade de segurança. A essência da configuração do nginx para o que pretendemos fazer com ele é o SSL e o PHP, mas existem alguns outros parâmetros do próprio nginx que também são importantes.

Além do nginx, é preciso fazer coisas demoradas com o OpenSSL e depois configurar o PHP. O nginx só estará pronto para os nossos propósitos depois que tudo isso estiver concluído.

nginx da fonte

Você pode instalar o nginx de duas formas: usando o SlackBuild padrão ou o meu SlackBuild. Se preferir usar o meu, geralmente a versão do nginx é mais nova que o padrão. O meu também compila os módulos ngx_pagespeed e HttpStripModule. Nenhum desses módulos é requerido durante toda a série, portanto fica a seu critério qual fonte do nginx usar.

Seja como for, você precisa enviar os arquivos do SlackBuild para o servidor e executá-lo. Se você possuir o sbopkg, também pode rodar sbopkg -i nginx para instalar o nginx a partir do SlackBuild padrão.

# cd ~/extra/nginx
# ./nginx.SlackBuild
# installpkg /tmp/nginx-1.8.0-x86_64-1_deny.tgz
# chmod 755 /etc/rc.d/rc.nginx
# /etc/rc.d/rc.nginx start

Seu servidor nginx estará no ar. Acesse-o através da url do seu domínio, que para fins de exemplo é http://mail.exemplo.com/. Você verá a página de boas vindas padrão do nginx. Agora precisamos configurá-lo.

nginx de doido

Vamos configurar tudo num tapa só. Depois veremos com mais detalhes o que foi feito.

Primeiro, edite o arquivo de configuração principal do nginx, /etc/nginx/nginx.conf. Apague todo o seu conteúdo e substitua por isso:

# nginx main settings
user                          nobody;                         # For URIs to be written by the nginx server, you must set write permissions to the user set in here.
                                                              # For php usage, remeber to set php-fpm user and listen.owner directives to this user.
                                                              #  Also, set the owner of php session.save_path (php -i | grep save_path) to this user.
worker_processes              1;
error_log                     /var/log/nginx/error.log error; # Default error log.

events {
    use                       epoll;                          # Important on Linux for performance.
    worker_connections        256;                            # Lower to 256 concurrent connections. Default: 1024.
    accept_mutex              off;                            # Turn this 'on' if worker_processes > 1.
                                                              #  No need for locking mutex on single worker, thus servers accepts faster.
}

# Global settings
http {
    default_type              application/octet-stream;       # Set default mime type.
    ignore_invalid_headers    on;                             # Set strict headers.
    include                   mime.types;                     # Load additional mime types.
    index                     index.html;                     # Set default indexes.
    keepalive_requests        20;                             # Default: 100.
    keepalive_timeout         300 300;                        # Default 65; made this bigger because creating new TCP connections.
                                                              #  is "expensive" and this helps to speed things along because open.
                                                              #  TCP connections can be re-used more often.
    recursive_error_pages     on;                             # Allows the use of "error_pages" directive.
    reset_timedout_connection on;                             # Close stale connections
    sendfile                  on;                             # sendfile(2) allows to transfer data from a file descriptor to another directly in kernel space.
    sendfile_max_chunk        512K;                           # Only used when sendfile=on; Set accordingly, see documentation.
    server_tokens             off;                            # Disables showing the nginx version number in auto generated error pages.
    tcp_nodelay               on;                             # Disable Nagle buffering algoritm. Helps sending small objects quickly.
    tcp_nopush                on;                             # Default: off; Cannot be used without sendfile() anyway.
    resolver                  172.16.0.2 valid=300s;          # DNS resolver.
    resolver_timeout          1s;                             # DNS resolver timeout.

    ### Limit requests
    # Works together with the "limit" in the server{..} directive to limit requests per IP.
    limit_req_zone            $binary_remote_addr zone=server:1m rate=60r/m;

    ### Compression
    gzip                      on;                             # On-the-fly compression. Static HTML and CSS compress well.
                                                              #  Compressing on-the-fly also adds delay!
    gzip_comp_level           9;                              # From 1-9, 9 being the maximum compression.
    gzip_http_version         1.1;                            # Set this to 1.1 to make it compatible with AWS CloudFront compressed requests.
    gzip_proxied              any;                            # Let CloudFront get the compressed assets.
    gzip_min_length           150;                            # Compress files larger than 150 bytes. Disable when using gzip_static.
                                                              #  Akamai uses 860 bytes as minimum to start compressing.
                                                              #  Google recommends starting at 150 bytes.
    gzip_vary                 on;                             # Let clients know we can compress data.
    # Set mime types subject to compression
    gzip_types application/ecmascript application/javascript application/json application/pdf application/postscript application/x-javascript;
    gzip_types font/opentype application/font-woff application/vnd.ms-fontobject application/x-font-ttf image/svg+xml image/x-icon;
    gzip_types text/css text/csv text/javascript text/plain text/xml;

    ### PHP related settings
    # PHP max upload limit cannot be larger than this
    client_max_body_size      13m;

    ### Load servers
    include                   /etc/nginx/conf.d/*.conf;       # Load server blocks.
}

Agora crie o arquivo /etc/nginx/conf.d/10-backend.conf com este conteúdo:

# Backend server: ViMbAdmim administration
server {
    listen      3443 ssl spdy rcvbuf=1k sndbuf=128k backlog=12;                    # The HTTPS server on port 3443.
    server_name mail.exemplo.com;

    # SSL settings
    # According to security recommendations from:
    # https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
    ssl_certificate           /etc/ssl/private/certificate.crt;
    ssl_certificate_key       /etc/ssl/private/certificate.key;

    ssl_ciphers               'AES128+EECDH:AES128+EDH:!aNULL';
    ssl_protocols             TLSv1 TLSv1.1 TLSv1.2;
    ssl_session_timeout       5m;
    ssl_session_cache         shared:SSL:5m;

    ssl_stapling              on;
    ssl_stapling_verify       on;
    ssl_prefer_server_ciphers on;
    ssl_dhparam               /etc/ssl/private/dhparam.pem;

    add_header                Strict-Transport-Security "max-age=63072000; includeSubDomains";
    add_header                X-Frame-Options DENY;
    add_header                X-Content-Type-Options nosniff;

    # Server settings
    root        /var/www/backend/public;                                           # The document root.
    index       index.php;                                                         # Set default indexes.
    limit_req   zone=server burst=200 nodelay;                                     # Limit requests per IP.
    add_header  Cache-Control public;                                              # Tell clients/proxies it's OK to cache.
    expires     max;                                                               # Cache as long as possible.
    access_log  /var/log/nginx/backend.access.log;                                 # Specfic log to backend services.

    # Load extensions
    include     php;                                                               # Load php in this server.
    include     restrictions;                                                      # Load per server restrictions.

    # ViMbAdmin environment settings
    location    / {
        fastcgi_param  APPLICATION_ENV production;
        try_files      $uri $uri/ /index.php?$args;
    }
}

Crie mais um arquivo, /etc/nginx/php, e coloque isso nele:

# Enable php by server.
# Designed to be included in any server {} block that must run php.
# These settings are based on the following guides:
# http://superuser.com/a/722000
# http://wiki.nginx.org/PHPFcgiExample

# Pass all .php files onto a php-fpm/php-fcgi server.
location ~ ^.+\.php(/|$) {
    fastcgi_split_path_info ^((?U).+\.php)(/?.+)$;

    # Zero-day exploit defense.
    if (!-f $document_root$fastcgi_script_name) {
        return 404;
    }

    # For debug only
    #fastcgi_intercept_errors on;

    fastcgi_pass  unix:/var/run/php5-fpm.sock;
    fastcgi_index index.php;
    include       fastcgi_params;
}

Mais um, /etc/nginx/fastcgi_params. Substitua todo o conteúdo por isso:

# These settings are based on the following guides:
# http://superuser.com/a/722000
# http://wiki.nginx.org/PHPFcgiExample

fastcgi_param   QUERY_STRING            $query_string;
fastcgi_param   REQUEST_METHOD          $request_method;
fastcgi_param   CONTENT_TYPE            $content_type;
fastcgi_param   CONTENT_LENGTH          $content_length;

fastcgi_param   SCRIPT_FILENAME         $document_root$fastcgi_script_name;
fastcgi_param   SCRIPT_NAME             $fastcgi_script_name;
fastcgi_param   PATH_INFO               $fastcgi_path_info;
fastcgi_param   PATH_TRANSLATED         $document_root$fastcgi_path_info;
fastcgi_param   REQUEST_URI             $request_uri;
fastcgi_param   DOCUMENT_URI            $document_uri;
fastcgi_param   DOCUMENT_ROOT           $document_root;
fastcgi_param   SERVER_PROTOCOL         $server_protocol;

fastcgi_param   GATEWAY_INTERFACE       CGI/1.1;
fastcgi_param   SERVER_SOFTWARE         nginx/$nginx_version;

fastcgi_param   REMOTE_ADDR             $remote_addr;
fastcgi_param   REMOTE_PORT             $remote_port;
fastcgi_param   SERVER_ADDR             $server_addr;
fastcgi_param   SERVER_PORT             $server_port;
fastcgi_param   SERVER_NAME             $server_name;

fastcgi_param   HTTPS                   $https;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param   REDIRECT_STATUS         200;

E outro, /etc/nginx/restrictions, com esse conteúdo:

# Restrictions configuration file.
# Designed to be included in any server {} block.

# Disable logging for favicons and robots
location = apple-touch-icon*.png {
    log_not_found off;
    access_log off;
}
location = favicon.ico {
    log_not_found off;
    access_log off;
}
location = robots.txt {
    allow all;
    log_not_found off;
    access_log off;
}

# Return error for attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).
# The same happens for scripts not available on this server, even if they exists.
# Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban)
location ~ /\.[h|D] {
    return 404;
}
location ~ (aspx|asp|cig|jsp|php|rb)$ {
    return 404;
}

E mais um, /etc/nginx/basicauth, com o conteúdo:

auth_basic "Restricted Area";
auth_basic_user_file /etc/nginx/htpasswd;

Agora crie o script /usr/local/bin/htpasswd com esse conteúdo:

#!/bin/bash
######################################################
# written by George Liu (eva2000) vbtechsupport.com
######################################################
# Create at /usr/local/nginx/conf/htpasswd
# ./htpasswd.sh create /usr/local/nginx/conf/htpasswd user1 pass1  
######################################################
# Append at /usr/local/nginx/conf/htpasswd
# ./htpasswd.sh append /usr/local/nginx/conf/htpasswd user2 pass2 
######################################################
# variables
#############
DT=`date +"%d%m%y-%H%M%S"`

######################################################
# functions
#############

genpassa() {
  if [[ -f "$file" && "$user" && "$pass" ]]; then
    printf "${user}:$(openssl passwd -apr1 ${pass})\n" >> $file
    echo ""
    echo "$file contents:"
    cat $file
  fi
}

genpassc() {
  if [[ -f "$file" && "$user" && "$pass" ]]; then
    printf "${user}:$(openssl passwd -apr1 ${pass})\n" > $file
    echo ""
    echo "$file contents:"
    cat $file
  fi
}

######################################################
file=$2
user=$3
pass=$4

case "$1" in
  create)
    touch $file
    genpassc
  ;;
  append)
    genpassa
  ;;
  *)
    echo ""
    echo "$0 {create|append} /usr/local/nginx/conf/htpasswd user1 pass1"
  ;;
esac

Altere as permissões do script e execute-o, substituindo <user> e <pass> por algo do seu agrado.

# chmod 755 /usr/local/bin/htpasswd
# htpasswd create /etc/nginx/htpasswd <user> <pass>
# chown nobody /etc/nginx/htpasswd
# chmod 400 /etc/nginx/htpasswd

Pronto, o ngix está configurado, mas se você reiniciá-lo agora vai ter problemas, porque ainda faltam muitas coisas externas a ele.

nginx paranóico

Vamos parar por um instante para entender um pouco do que aconteceu aí em cima. Só explico as partes essenciais. O resto você pode descobrir na documentação do nginx e nos links que aparecem nos comentários das configurações.

A primeira coisa importante é no arquivo /etc/nginx/nginx.conf: user nobody;. Essa linha diz que o nginx vai rodar com o usuário nobody. Esse usuário não possui nenhum privilégio especial, logo a superfície vulnerável caso o nginx seja comprometido é mínima.

Note que a configuração principal do nginx não possui nenhuma configuração de servidor (blocos server{}). Ele espera que esses blocos estejam em /etc/nginx/conf.d/*.conf.

E lá existe o 10-backend.conf. Essa configuração diz para o nginx criar um servidor SSL na porta 3443, mas pode ser outra se você quiser, até mesmo a 443 padrão.

Como esse é um servidor SSL, ele precisa de certificados. Lembra dos pé-requisitos que citei na primeira parte? Então, pegue o seu certificado e a chave privada dele e coloque em /etc/ssl/private/certificate.crt e /etc/ssl/private/certificate.key, respectivamente.

Há outro arquivo importantíssimo para o SSL, /etc/ssl/private/dhparam.pem. Esse arquivo contém as chaves utilizadas para gerar outras chaves efêmeras requeridas pelo recurso 'segurança de volta para o futuro III', também conhecido pela alcunha Perfect Forward Secrecy, ou simplesmente PFS, do OpenSSL. O OpenSSL já vem com um arquivo desse, mas ele é ruim por conter chaves fracas, de 1024 bit apenas. Precisamos de algo melhor. Então rode isso:

# cd /etc/ssl/private
# openssl dhparam -out dhparam.pem 4096

Isso demora uns bons pares de minutos. Se quiser rodar num screen, fique a vontade.

Agora, isso é muito importante: proteja seus arquivos SSL. A segurança de todo o sistema depende exclusivamente deles e não é só o ngix que irá utilizá-los!!!

# chown root:root /etc/ssl/private/*.*
# chmod 400 /etc/ssl/private/*.*

Ainda sobre o SSL, também tornamos o nginx mais seguro ao limitar as cifras e protocolos aceitos para as conexões criptografadas, o que é feito pelos parâmetros ssl_ciphers e ssl_protocols. Isso elimina a compatibilidade com navegadores antigos (cof, IE≤8, cof), mas quem precisa deles?

Por fim, criamos um método de autenticação próprio para o nginx através da configuração /etc/nginx/basicauth. Esse método de autenticação é o mais simples possível e não contém qualquer tipo de criptografia, ou seja, os dados são enviados limpos do cliente para o servidor. Por isso, é importante que ele seja utilizado somente em conexões com SSL.

O script /usr/local/bin/htpasswd é o responsável por criar o arquivo de senhas /etc/nginx/htpasswd. O nginx usa isso para autenticar os usuários em locais que você decide não expor para toda a internet.

Mas se você prestar atenção, notará que não há a inclusão desta proteção adicional em nenhum lugar. Os arquivos estão lá, eles funcionam, mas não são utilizados. Há uma causa circunstancial para isso.

Notou que nosso servidor web que atende o backend está configurado para rodar na porta 3443? Pois bem, como no contexto desta série de artigos estamos usando o AWS EC2, ele contém um negócio chamado 'Security Groups'. Isso nada mais é do que o firewall do EC2. Nosso servidor terá um firewall local, via iptables, que não depende dos security groups (SG), e também os SGs. Isso é efetivamente uma camada dupla de firewall.

Nestas duas camadas de proteção, teremos regras assim:

  1. No firewall local, aceitar todas as conexões entrantes na porta 3443 vindas de qualquer IP na internet.
  2. No security group do AWS, aceitar todas as conexões entrantes em todas as portas apenas para o seu IP.

Deste modo, somente o IP de origem do seu computador ou da rede em que você está poderá ter acesso ao seu backend de administração de contas do servidor. Para todo o resto da internet esse serviço não existe. Isso torna menos eficaz haver uma autenticação adicional para isso, principalmente porque ela não é integrada a nada.

'Mas o meu IP é dinâmico! Tem que ficar mudando toda hora?', você brada. Tem. Mas se você for bonzinho, no final eu mostro como automatizar isso.

Se você não puder ou não confiar nisso e quiser a autenticação assim mesmo, basta fazer isso no bloco location{} do arquivo /etc/nginx/conf.d/10-backend.conf:

# ViMbAdmin environment settings
location    / {
    include        basicauth;
    fastcgi_param  APPLICATION_ENV production;
    try_files      $uri $uri/ /index.php?$args;
}

Essa parte já está bem longa. Vou parar por aqui e na próxima falamos dos detalhes referentes ao PHP.

Ah! Você já pode reiniciar o nginx agora, só que a página de boas vindas já era e o PHP ainda não está funcionando. Tipo, ele é meio inútil neste ponto, um inútil bem seguro.