Tutorial

Como proteger um aplicativo Node.js em contêiner com Nginx, Let's Encrypt e Docker Compose

Node.jsSecurityDockerLet's EncryptUbuntu 18.04

Introdução

Existem várias maneiras de melhorar a flexibilidade e segurança do seu aplicativo Node.js. O uso de um proxy reverso como o Nginx oferece a você a capacidade de carregar solicitações de balanceamento de carga, conteúdo de cache estático e de* implementar a Segurança em Camada*s de Transporte (TLS). Ao habilitar o HTTPS criptografado no seu servidor, garante-se que a comunicação para o seu aplicativo e vinda dele permaneça segura.

A implementação de um proxy reverso com TLS/SSL em contêineres envolve um conjunto diferente de procedimentos do trabalho em um sistema operacional de host. Por exemplo, se estivesse obtendo certificados do Let’s Encrypt para um aplicativo em execução em um servidor, deveria instalar o software necessário diretamente no seu host. Os contêineres permitem que você utilize uma abordagem diferente. Ao usar o Docker Compose, é possível criar contêineres para o seu aplicativo, seu servidor Web e o cliente Certbot que permite você de obter seus certificados. Ao seguir estes passos, você pode aproveitar a modularidade e a portabilidade de um fluxo de trabalho em contêiner.

Neste tutorial, será implantado um aplicativo Node.js com um proxy reverso Nginx usando o Docker Compose. Você receberá certificados TLS/SSL para o domínio associados ao seu aplicativo e garantirá que ele receba uma classificação de segurança elevada do SSL Labs. Por fim, será configurado um trabalho cron para renovar seus certificados para que seu domínio permaneça seguro.

Pré-requisitos

Para seguir este tutorial, será necessário:

  • Um servidor Ubuntu 18.04, um usuário não raiz com privilégios sudo e um firewall ativo. Para saber como configurar isso, consulte este guia de configuração inicial do servidor.
  • O Docker e o Docker Compose instalados no seu servidor. Como orientação na instalação do Docker, siga os Passos 1 e 2 de Como instalar e usar o Docker no Ubuntu 18.04. Como orientação na instalação do Compose, siga o Passo 1 de Como instalar o Docker Compose no Ubuntu 18.04.
  • Um nome de domínio registrado. Este tutorial usará o example.com do início ao fim. Você pode obter um domínio gratuitamente através do Freenom, ou usar o registrador de domínios de sua escolha.
  • Ambos os registros de DNS a seguir serão configurados para o seu servidor. Você pode seguir esta introdução para o DNS da DigitalOcean para obter mais detalhes sobre como adicioná-los a uma conta da DigitalOcean, caso seja o que estiver usando:

    • Um registro A com example.com apontando para o endereço de IP público do seu servidor.
    • Um registro A com example.com apontando para o endereço IP público do seu servidor.

Passo 1 — Clonando e testando o aplicativo Node

Como primeiro passo, clonaremos o repositório com o código do aplicativo Node, que inclui o Dockerfile que usaremos na construção da nossa imagem de aplicativo com o Compose. Podemos testar primeiro o aplicativo a partir de sua construção e execução com o comando docker run, sem um proxy reverso ou SSL.

No diretório home do seu usuário não raiz, clone o repositório nodejs-image-demo da conta GitHub da Comunidade DigitalOcean. Este repositório inclui o código da configuração descrito em Como construir um aplicativo Node.js com o Docker.

Clone o repositório em um diretório chamado node_project:

  • git clone https://github.com/do-community/nodejs-image-demo.git node_project

Vá para o diretório node_project:

  • cd node_project

Neste diretório, há um Dockerfile que contém instruções para a construção de um aplicativo Node usando a imagem do Docker node:10 e o conteúdo do seu diretório de projeto atual. Você pode olhar o conteúdo do Dockerfile digitando:

  • cat Dockerfile
Output
FROM node:10-alpine RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app WORKDIR /home/node/app COPY package*.json ./ USER node RUN npm install COPY --chown=node:node . . EXPOSE 8080 CMD [ "node", "app.js" ]

Essas instruções constroem uma imagem do Node através da cópia do código do projeto do diretório atual para o contêiner e instalação de dependências com o npm install. Elas também se aproveitam do salvamento em cache e disposição em camadas da imagem. Isso é feito pela separação da cópia do package.json e package-lock.json, que contém as dependências listadas do projeto, da cópia do resto do código do aplicativo. Por fim, as instruções especificam que o contêiner será executado como o usuário node não raiz com as permissões apropriadas definidas no código do aplicativo e no diretório node_modules.

Para obter mais informações sobre este Dockerfile e práticas recomendadas da imagem do Node, consulte a discussão completa no Passo 3 de Como construir um aplicativo Node.js com o Docker.

Para testar o aplicativo sem o SSL, construa e identifique a imagem usando o docker build e o sinalizador -t. Vamos nomear a imagem node-demo, mas você pode dar a ela o nome que quiser:

  • docker build -t node-demo .

Assim que o processo de construção for concluído, você pode listar suas imagens com o docker images:

  • docker images

Você verá o seguinte resultado, confirmando a compilação da imagem do aplicativo:

Output
REPOSITORY TAG IMAGE ID CREATED SIZE node-demo latest 23961524051d 7 seconds ago 73MB node 10-alpine 8a752d5af4ce 3 weeks ago 70.7MB

Em seguida, crie o contêiner com o docker run. Vamos incluir três sinalizadores com este comando:

  • -p: publica a porta no contêiner e a mapeia para uma porta no nosso host. Usaremos a porta 80 no host, mas sinta-se a vontade para escolher outra se necessário, caso tenha outro processo em execução naquela porta. Para obter mais informações sobre como isso funciona, veja esta discussão nos documentos do Docker sobre associação de portas.
  • -d: executa o contêiner em segundo plano.
  • --name: permite-nos dar ao contêiner um nome memorável.

Execute o comando a seguir para criar o contêiner:

  • docker run --name node-demo -p 80:8080 -d node-demo

Verifique seus contêineres em execução com o docker ps:

  • docker ps

Você verá o resultado que confirma que o seu contêiner do aplicativo está funcionando:

Output
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4133b72391da node-demo "node app.js" 17 seconds ago Up 16 seconds 0.0.0.0:80->8080/tcp node-demo

Agora, você pode visitar seu domínio para testar sua configuração: http://example.com. Lembre-se de substituir o example.com pelo seu próprio nome de domínio. Seu aplicativo exibirá a seguinte página de destino:

Application Landing Page

Agora que você testou o aplicativo, pare o contêiner e remova as imagens. Use o docker ps novamente para obter seu CONTAINER ID:

  • docker ps
Output
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4133b72391da node-demo "node app.js" 17 seconds ago Up 16 seconds 0.0.0.0:80->8080/tcp node-demo

Pare o contêiner com o docker stop. Certifique-se de substituir o CONTAINER ID listado aqui pelo CONTAINER ID do seu aplicativo:

  • docker stop 4133b72391da

Agora, é possível remover o contêiner parado e todas as imagens, incluindo imagens não utilizadas e penduradas, com o docker system prune e o sinalizador -a:

  • docker system prune -a

Digite y quando solicitado na saída para confirmar que você gostaria de remover o contêiner e imagens parados. Fique ciente de que isso também removerá seu cache de construção.

Com sua imagem de aplicativo testada, siga em frente para a construção do resto da sua configuração com o Docker Compose.

Passo 2 — Definindo as configurações do servidor Web

Com nosso aplicativo Dockerfile funcionando, podemos criar um arquivo de configuração para executar nosso contêiner Nginx. Começaremos com uma configuração mínima que incluirá nosso nome de domínio, root do documento, informações de proxy e um bloco de localização para dirigir os pedidos do Certbot ao diretório .well-known. Lá, ele colocará um arquivo temporário para validar que o DNS para nosso domínio resolva para nosso servidor.

Primeiramente, crie um diretório no diretório atual do projeto para o arquivo de configuração:

  • mkdir nginx-conf

Abra o arquivo com o nano ou com o seu editor favorito:

  • nano nginx-conf/nginx.conf

Adicione o seguinte bloco de servidor para servir como proxy para os pedidos de usuário para o contêiner do seu aplicativo Node e redirecionar os pedidos do Certbot ao diretório .well-known. Certifique-se de substituir o example.com pelo seu próprio nome de domínio:

~/node_project/nginx-conf/nginx.conf
server {
        listen 80;
        listen [::]:80;

        root /var/www/html;
        index index.html index.htm index.nginx-debian.html;

        server_name example.com www.example.com;

        location / {
                proxy_pass http://nodejs:8080;
        }

        location ~ /.well-known/acme-challenge {
                allow all;
                root /var/www/html;
        }
}

Este bloco de servidor nos permitirá iniciar o contêiner Nginx como um proxy reverso, que passará pedidos para nosso contêiner do aplicativo Node. Ele também nos permitirá usar o plug-in webroot do Certbot para obter certificados para nosso domínio. Este plug-in depende do método de validação HTTP-01, que usa um pedido HTTP para provar que o Certbot pode acessar recursos de um servidor que responde a um dado nome de domínio.

Assim que terminar a edição, salve e feche o arquivo. Para aprender mais sobre o servidor Nginx e os algoritmos de blocos de localização, consulte este artigo Entendendo o servidor Nginx e os algoritmos de seleção de blocos de localização.

Com os detalhes de configuração do servidor Web funcionando, podemos seguir em frente para a criação do nosso arquivo docker-compose.yml, que nos permitirá criar nossos serviços de aplicativo e o contêiner do Certbot que usaremos para obter nossos certificados.

Passo 3 — Criando o arquivo do Docker Compose

O arquivo docker-compose.yml definirá nossos serviços, incluindo o aplicativo Node e o servidor Web. Ele especificará detalhes como volumes nomeados, que serão críticos para compartilhar credenciais SSL entre contêineres, além de informações de rede e portas. Ele também nos permitirá especificar comandos específicos para serem executados quando nossos contêineres forem criados. Este arquivo é o recurso central que definirá como nossos serviços funcionarão em conjunto.

Abra o arquivo no seu diretório atual:

  • nano docker-compose.yml

Primeiro, defina o serviço de aplicativo:

~/node_project/docker-compose.yml
version: '3'

services:
  nodejs:
    build:
      context: .
      dockerfile: Dockerfile
    image: nodejs
    container_name: nodejs
    restart: unless-stopped

A definição de serviço nodejs inclui o seguinte:

  • build: define as opções de configuração, incluindo o context e dockerfile, que serão aplicadas quando o Compose construir a imagem do aplicativo. Se quisesse usar uma imagem existente de um registro como o Docker Hub, poderia usar como alternativa a instrução image, com informações sobre seu nome de usuário, repositório e tag da imagem.
  • context: define o contexto de compilação para a compilação de imagem do aplicativo. Neste caso, é o diretório atual do projeto.
  • dockerfile: especifica o Dockerfile que o Compose usará para a compilação — o Dockerfile que você olhou no Passo 1.
  • image, container_name: aplicam nomes à imagem e contêiner.
  • restart: define a política de reinício. A padrão é no, mas definimos o contêiner para reiniciar a menos que ele seja interrompido.

Note que não estamos incluindo bind mounts com este serviço, uma vez que nossa configuração está focada na implantação e não no desenvolvimento. Para obter mais informações, consulte a documentação do Docker sobre bind mounts e volumes.

Para habilitar a comunicação entre os contêineres do aplicativo e do servidor Web, adicionaremos também uma rede bridge chamada app-network abaixo da definição de reinicialização:

~/node_project/docker-compose.yml
services:
  nodejs:
...
    networks:
      - app-network

Uma rede bridge definida pelo usuário permite a comunicação entre contêineres no mesmo host daemon do Docker. Isso simplifica o tráfego e a comunicação dentro do seu aplicativo, uma vez que todas as portas entre os contêineres na mesma rede bridge são abertas, ao mesmo tempo em que nenhuma porta é exposta ao mundo exterior. Assim, é possível ser seletivo abrindo apenas as portas que você precisar para expor seus serviços front-end.

Em seguida, defina o serviço webserver:

~/node_project/docker-compose.yml
...
 webserver:
    image: nginx:mainline-alpine
    container_name: webserver
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - web-root:/var/www/html
      - ./nginx-conf:/etc/nginx/conf.d
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
    depends_on:
      - nodejs
    networks:
      - app-network

Algumas das configurações que definimos para o serviço nodejs permanecem as mesmas, mas também fizemos as seguintes alterações:

Também especificamos os seguintes volumes nomeados e bind mounts:

  • web-root:/var/www/html: adicionará os ativos estáticos do nosso site, copiados para um volume chamado web-root, para o diretório /var/www/html no contêiner.
  • ./nginx-conf:/etc/nginx/conf.d: irá associar a montagem do diretório de configuração Nginx no host ao diretório relevante no contêiner, garantindo que quaisquer alterações que façamos em arquivos no host serão refletidas no contêiner.
  • certbot-etc:/etc/letsencrypt: irá montar os certificados e chaves relevantes do Let’s Encrypt do nosso domínio para o diretório apropriado no contêiner.
  • certbot-var:/var/lib/letsencrypt: monta o diretório de trabalho padrão do Let’s Encrypt para o diretório apropriado no contêiner.

Em seguida, adicione as opções de configuração para o contêiner certbot. Certifique-se de substituir as informações de domínio e e-mail pelo seu próprio nome de domínio e e-mail:

~/node_project/docker-compose.yml
...
  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html
    depends_on:
      - webserver
    command: certonly --webroot --webroot-path=/var/www/html --email sammy@example.com --agree-tos --no-eff-email --staging -d example.com  -d www.example.com

Esta definição diz ao Compose para puxar a imagem certbot/certbot do Docker Hub. Ela também usa volumes nomeados para compartilhar recursos com o contêiner do Nginx, incluindo os certificados de domínio e chaves no certbot-etc, o diretório de trabalho do Let’s Encrypt no certbot-var e o código do aplicativo no web-root.

Novamente, usamos o depends_on para especificar que o contêiner do certbot deve ser iniciado assim que o serviço webserver estiver funcionando.

Também incluímos uma opção command que especifica o comando a ser executado quando o contêiner for iniciado. Ele inclui o subcomando certonly com as seguintes opções:

  • --webroot: diz ao Certbot para usar o plug-in webroot para colocar arquivos na pasta webroot para autenticação.
  • --webroot-path: especifica o caminho do diretório webroot.
  • --email: seu e-mail escolhido para o registro e recuperação.
  • --agree-tos: especifica que você concorda com os termos do Acordo do Assinante do protocolo ACME.
  • --no-eff-email: diz ao Certbot que você não deseja compartilhar seu e-mail com a Electronic Frontier Foundation (EFF). Sinta-se à vontade para omitir isso se preferir.
  • --staging: diz ao Certbot que você deseja usar o ambiente de preparo do Let’s Encrypt para obter certificados de teste. Usar essa opção permite que você teste suas opções de configuração e evite possíveis limites de solicitação de domínio. Para obter mais informações sobre esses limites, consulte a documentação sobre limites de taxa do Let’s Encrypt.
  • -d: permite que você especifique os nomes de domínio que gostaria de aplicar ao seu pedido. Neste caso, incluímos o example.com e www.example.com. Certifique-se de substituí-los pelas suas próprias preferências de domínio.

Como passo final, adicione as definições de volume e rede. Certifique-se de substituir o nome de usuário presente aqui pelo seu usuário não raiz:

~/node_project/docker-compose.yml
...
volumes:
  certbot-etc:
  certbot-var:
  web-root:
    driver: local
    driver_opts:
      type: none
      device: /home/sammy/node_project/views/
      o: bind

networks:
  app-network:
    driver: bridge

Nossos volumes nomeados incluem nosso certificado Certbot e os volumes de diretórios de trabalho, além dos volume para os ativos estáticos do nosso site, o web-root. Na maioria dos casos, o driver padrão para os volumes do Docker é o driver local, que no Linux aceita opções semelhantes ao comando mount. Graças a isso, é possível especificar uma lista de opções de drivers com o driver_opts que montam o diretório views no host, sendo que estes contém os ativos estáticos do nosso aplicativo, além do volume em tempo de execução. O conteúdo do diretório pode ser, então, compartilhado entre contêineres. Para obter mais informações sobre o conteúdo do diretório views, consulte o Passo 2 de Como construir um aplicativo Node.js com o Docker.

O arquivo docker-compose.yml se parecerá com isto quando terminar:

~/node_project/docker-compose.yml
version: '3'

services:
  nodejs:
    build:
      context: .
      dockerfile: Dockerfile
    image: nodejs
    container_name: nodejs
    restart: unless-stopped
    networks:
      - app-network

  webserver:
    image: nginx:mainline-alpine
    container_name: webserver
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - web-root:/var/www/html
      - ./nginx-conf:/etc/nginx/conf.d
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
    depends_on:
      - nodejs
    networks:
      - app-network

  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html
    depends_on:
      - webserver
    command: certonly --webroot --webroot-path=/var/www/html --email sammy@example.com --agree-tos --no-eff-email --staging -d example.com  -d www.example.com

volumes:
  certbot-etc:
  certbot-var:
  web-root:
    driver: local
    driver_opts:
      type: none
      device: /home/sammy/node_project/views/
      o: bind

networks:
  app-network:
    driver: bridge  

Com as definições de serviço instaladas, você está pronto para iniciar os contêineres e testar seus pedidos de certificado.

Passo 4 — Obtendo certificados e credenciais SSL

Podemos iniciar nossos contêineres com o docker-compose up, que criará e executará nossos contêineres e serviços na ordem que especificamos. Se nossos pedidos de domínio forem bem sucedidos, veremos o status correto de saída no nosso resultado e os certificados corretos montados na pasta /etc/letsencrypt/live no contêiner webserver.

Crie os serviços com o docker-compose up e o sinalizador -d, os quais executarão os contêineres nodejs e webserver em segundo plano:

  • docker-compose up -d

Você verá um resultado confirmando que seus serviços foram criados:

Output
Creating nodejs ... done Creating webserver ... done Creating certbot ... done

Com o uso do docker-compose ps, verifique o status dos seus serviços:

  • docker-compose ps

Se tudo ocorreu bem, seus serviços nodejs e webserver devem estar Up e o contêiner certbot terá finalizado com uma mensagem de status 0:

Output
Name Command State Ports ------------------------------------------------------------------------ certbot certbot certonly --webroot ... Exit 0 nodejs node app.js Up 8080/tcp webserver nginx -g daemon off; Up 0.0.0.0:80->80/tcp

Se você ver qualquer outra coisa além de Up na coluna State para os serviços nodejs e webserver, ou um status de saída que não seja 0 para o contêiner certbot, certifique-se de verificar os registros de serviço com o comando docker-compose logs:

  • docker-compose logs service_name

Agora, é possível verificar se suas credenciais foram instaladas no contêiner webserver com o docker-compose exec:

  • docker-compose exec webserver ls -la /etc/letsencrypt/live

Se seu pedido foi bem sucedido, você verá um resultado similar a este:

Output
total 16 drwx------ 3 root root 4096 Dec 23 16:48 . drwxr-xr-x 9 root root 4096 Dec 23 16:48 .. -rw-r--r-- 1 root root 740 Dec 23 16:48 README drwxr-xr-x 2 root root 4096 Dec 23 16:48 example.com

Agora que você sabe que seu pedido será bem sucedido, edite a definição do serviço certbot para remover o sinalizador --staging.

Abra o docker-compose.yml:

  • nano docker-compose.yml

Encontre a seção do arquivo com a definição de serviço do certbot e substitua o sinalizador --staging na opção command pelo sinalizador --force-renewal, o qual dirá ao Certbot que você quer solicitar um novo certificado com os mesmos domínios de um certificado existente. A definição de serviço do certbot deve agora se parecer com isto:

~/node_project/docker-compose.yml
...
  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html
    depends_on:
      - webserver
    command: certonly --webroot --webroot-path=/var/www/html --email sammy@example.com --agree-tos --no-eff-email --force-renewal -d example.com -d www.example.com
...

Agora, é possível executar o docker-compose up para recriar o contêiner do certbot e seus volumes relevantes. Também vamos incluir a opção --no-deps para dizer ao Compose que ele pode pular a inicialização do serviço webserver, já que ele já está em funcionamento:

  • docker-compose up --force-recreate --no-deps certbot

Você verá o resultado indicando que o seu pedido de certificado foi bem-sucedido:

Output
certbot | IMPORTANT NOTES: certbot | - Congratulations! Your certificate and chain have been saved at: certbot | /etc/letsencrypt/live/example.com/fullchain.pem certbot | Your key file has been saved at: certbot | /etc/letsencrypt/live/example.com/privkey.pem certbot | Your cert will expire on 2019-03-26. To obtain a new or tweaked certbot | version of this certificate in the future, simply run certbot certbot | again. To non-interactively renew *all* of your certificates, run certbot | "certbot renew" certbot | - Your account credentials have been saved in your Certbot certbot | configuration directory at /etc/letsencrypt. You should make a certbot | secure backup of this folder now. This configuration directory will certbot | also contain certificates and private keys obtained by Certbot so certbot | making regular backups of this folder is ideal. certbot | - If you like Certbot, please consider supporting our work by: certbot | certbot | Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate certbot | Donating to EFF: https://eff.org/donate-le certbot | certbot exited with code 0

Com seus certificados instalados, você pode seguir em frente para modificar sua configuração do Nginx para incluir o SSL.

Passo 5 — Modificando as configuração do servidor Web e da definição de serviço

Habilitar o SSL na nossa configuração Nginx envolverá a adição de um redirecionamento do HTTP para o HTTPS e a especificação dos nossos certificados e locais de chave SSL. Isso também envolverá especificar nosso grupo Diffie-Hellman, que usaremos para o Perfect Forward Secrecy.

Como será recriado o serviço webserver para incluir essas adições, você pode interrompê-lo agora:

  • docker-compose stop webserver

Em seguida, crie um diretório no seu diretório atual de projeto para sua chave Diffie-Hellman:

  • mkdir dhparam

Gere sua chave com o comando openssl:

  • sudo openssl dhparam -out /home/sammy/node_project/dhparam/dhparam-2048.pem 2048

A chave será gerada após alguns instantes.

Para adicionar as informações relevantes do Diffie-Hellman e SSL na sua configuração do Nginx, remova primeiro o arquivo de configuração do Nginx que você criou mais cedo:

  • rm nginx-conf/nginx.conf

Abra outra versão do arquivo:

  • nano nginx-conf/nginx.conf

Adicione o seguinte código ao arquivo para redirecionar o HTTP para o HTTPS e adicione credenciais, protocolos e cabeçalhos de segurança do protocolo SSL. Lembre-se de substituir o example.com pelo seu próprio domínio:

~/node_project/nginx-conf/nginx.conf

server {
        listen 80;
        listen [::]:80;
        server_name example.com www.example.com;

        location ~ /.well-known/acme-challenge {
          allow all;
          root /var/www/html;
        }

        location / {
                rewrite ^ https://$host$request_uri? permanent;
        }
}

server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;
        server_name example.com www.example.com;

        server_tokens off;

        ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

        ssl_buffer_size 8k;

        ssl_dhparam /etc/ssl/certs/dhparam-2048.pem;

        ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
        ssl_prefer_server_ciphers on;

        ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;

        ssl_ecdh_curve secp384r1;
        ssl_session_tickets off;

        ssl_stapling on;
        ssl_stapling_verify on;
        resolver 8.8.8.8;

        location / {
                try_files $uri @nodejs;
        }

        location @nodejs {
                proxy_pass http://nodejs:8080;
                add_header X-Frame-Options "SAMEORIGIN" always;
                add_header X-XSS-Protection "1; mode=block" always;
                add_header X-Content-Type-Options "nosniff" always;
                add_header Referrer-Policy "no-referrer-when-downgrade" always;
                add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;
                # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
                # enable strict transport security only if you understand the implications
        }

        root /var/www/html;
        index index.html index.htm index.nginx-debian.html;
}

O bloco de servidor HTTP especifica o webroot dos pedidos de renovação do Certbot para o diretório .well-known/acme-challenge. Isso também inclui uma diretriz de reescrita, que direciona os pedidos do HTTP para o diretório raiz para o HTTPS.

O bloco de servidor HTTPS habilita o ssl e o http2. Para ler mais sobre como o HTTP/2 itera nos protocolos HTTP e os benefícios que ele pode ter para o desempenho do site, consulte a introdução de Como configurar o Nginx com suporte HTTP/2 no Ubuntu 18.04. Este bloco também inclui uma série de opções para garantir que você esteja usando os protocolos e criptografias SSL mais atualizados e que o grampeamento OSCP esteja ligado. O grampeamento OSCP permite a oferta de uma resposta com a data marcada da sua autoridade de certificação durante o handshake TLS, o que pode acelerar o processo de autenticação.

O bloco também especifica suas credenciais e locais de chave do SSL e Diffie-Hellman.

Por fim, transferimos as informações de passagem de proxy para este bloco, incluindo um bloco de localização com uma diretriz try_files, direcionando pedidos para nosso contêiner do aplicativo Node.js de alias, e um bloco de localização para aquele alias, que inclui cabeçalhos de segurança que nos permitirão obter classificações A em coisas como os sites de teste de servidor SSL Labs e Security Headers. Estes cabeçalhos incluem o X-Frame-Options, X-Content-Type-Options, Referrer Policy, Content-Security-Policy, e X-XSS-Protection. O cabeçalho HTTP Strict Transport Security (HSTS) é retirado do comentário - habilite isso apenas se você entender as implicações e avaliou sua funcionalidade de “precarregamento”.

Assim que terminar a edição, salve e feche o arquivo.

Antes de recriar o serviço webserver, será necessário adicionar algumas coisas na definição de serviço no seu arquivo docker-compose.yml, incluindo informações relevantes de porta para o HTTPS e uma definição de volume do Diffie-Hellman.

Abra o arquivo:

  • nano docker-compose.yml

Na definição do serviço webserver, adicione o seguinte mapeamento de portas e o volume nomeado dhparam:

~/node_project/docker-compose.yml
...
 webserver:
    image: nginx:latest
    container_name: webserver
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - web-root:/var/www/html
      - ./nginx-conf:/etc/nginx/conf.d
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - dhparam:/etc/ssl/certs
    depends_on:
      - nodejs
    networks:
      - app-network

Em seguida, adicione o volume dhparam às suas definições de volumes:

~/node_project/docker-compose.yml
...
volumes:
  ...
  dhparam:
    driver: local
    driver_opts:
      type: none
      device: /home/sammy/node_project/dhparam/
      o: bind

De maneira similar ao volume web-root, o volume dhparam montará a chave Diffie-Hellman armazenada no host para o contêiner webserver.

Salve e feche o arquivo quando você terminar a edição.

Recrie o serviço webserver:

  • docker-compose up -d --force-recreate --no-deps webserver

Verifique seus serviços com o docker-compose ps:

  • docker-compose ps

Você deve ver um resultado indicando que seus serviços nodejs e webserver estão funcionando:

Output
Name Command State Ports ---------------------------------------------------------------------------------------------- certbot certbot certonly --webroot ... Exit 0 nodejs node app.js Up 8080/tcp webserver nginx -g daemon off; Up 0.0.0.0:443->443/tcp, 0.0.0.0:80->80/tcp

Por fim, visite seu domínio para garantir que tudo está funcionando conforme esperado. Navegue com seu browser até https://example.com, certificando-se de substituir o example.com pelo seu próprio nome de domínio. Você verá a seguinte página de destino:

Application Landing Page

Você também deve ver o ícone de cadeado no indicador de segurança do seu navegador. Se quiser, navegue até a página de destino do teste de servidor do SSL Labs, ou a página de destino do teste de servidor do Security Headers. As opções de configuração que incluímos devem garantir ao seu site uma classificação A em ambos.

Passo 6 — Renovando certificados

Os certificados do Let’s Encrypt são válidos por 90 dias, então você vai querer configurar um processo de renovação automatizado para garantir que eles não expirem. Uma maneira de fazer isso é criando um trabalho com o utilitário de agendamento cron. Neste caso, vamos agendar uma tarefa do cron usando um script que renovará nossos certificados e recarregará nossa configuração do Nginx.

Abra um script chamado ssl_renew.sh no seu diretório de projeto:

  • nano ssl_renew.sh

Adicione o seguinte código ao script para renovar seus certificados e recarregar a configuração do seu servidor Web:

~/node_project/ssl_renew.sh
#!/bin/bash

COMPOSE="/usr/local/bin/docker-compose --no-ansi"
DOCKER="/usr/bin/docker"

cd /home/sammy/node_project/
$COMPOSE run certbot renew --dry-run && $COMPOSE kill -s SIGHUP webserver
$DOCKER system prune -af

Primeiro, este script atribui o binário docker-compose a uma variável chamada COMPOSE e especifica a opção --no-ansi, a qual executará os comandos do docker-compose sem os caracteres de controle do ANSI. Em seguida, ele faz o mesmo com o binário docker. Por fim, ele muda para o diretório do projeto ~/wordpress e executa os seguintes comandos docker-compose:

  • docker-compose run: iniciará um contêiner certbot e substituirá o comando fornecido em nossa definição de serviço certbot. Em vez de usar o subcomando certonly vamos usar o subcomando renew aqui, o qual renovará os certificados que estão próximos de expirar. Incluímos a opção --dry-run aqui para testar nosso script.
  • docker-compose kill: enviará um sinal SIGHUP para o contêiner webserver recarregar a configuração do Nginx. Para obter mais informações sobre o uso deste processo para recarregar sua configuração do Nginx, consulte este post do blog do Docker sobre a implantação da imagem oficial do Nginx com o Docker.

Na sequência, ele executa o docker system prune para remover todos os contêineres e imagens não utilizados.

Feche o arquivo quando terminar a edição. Torne-o executável:

  • chmod +x ssl_renew.sh

Em seguida, abra seu arquivo root crontab para executar o script de renovação em um intervalo especificado:

  • sudo crontab -e

Se esta for a primeira vez que você edita esse arquivo, será solicitado que escolha um editor:

crontab
no crontab for root - using an empty one
Select an editor.  To change later, run 'select-editor'.
  1. /bin/ed
  2. /bin/nano        <---- easiest
  3. /usr/bin/vim.basic
  4. /usr/bin/vim.tiny
Choose 1-4 [2]:
...

No final do arquivo, adicione a seguinte linha:

crontab
...
*/5 * * * * /home/sammy/node_project/ssl_renew.sh >> /var/log/cron.log 2>&1

Isso definirá intervalos de trabalho de cinco minutos cada, para que você possa testar se o seu pedido de renovação funcionou como o previsto. Também criamos um arquivo de registro, cron.log, para gravar o resultado relevante do trabalho.

Após cinco minutos, verifique o cron.log para ver se o pedido de renovação foi bem-sucedido:

  • tail -f /var/log/cron.log

Um resultado confirmando uma renovação bem-sucedida deve aparecer:

Output
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates below have not been saved.) Congratulations, all renewals succeeded. The following certs have been renewed: /etc/letsencrypt/live/example.com/fullchain.pem (success) ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates above have not been saved.) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Killing webserver ... done

Agora, é possível modificar o arquivo crontab para definir um intervalo diário. Para executar o script todos os dias ao meio-dia, por exemplo, você modificaria a última linha do arquivo para que fique com a seguinte aparência:

crontab
...
0 12 * * * /home/sammy/node_project/ssl_renew.sh >> /var/log/cron.log 2>&1

Você também vai querer remover a opção --dry-run do seu script ssl_renew.sh:

~/node_project/ssl_renew.sh
#!/bin/bash

COMPOSE="/usr/local/bin/docker-compose --no-ansi"
DOCKER="/usr/bin/docker"

cd /home/sammy/node_project/
$COMPOSE run certbot renew && $COMPOSE kill -s SIGHUP webserver
$DOCKER system prune -af

Seu trabalho cron irá garantir que seus certificados do Let’s Encrypt não expirem, renovando-os quando forem elegíveis para tanto. Você também pode configurar um rodízio de registros com o utilitário Logrotate para rodiziar e comprimir seus arquivos de registro.

Conclusão

Você usou contêineres para configurar e executar um aplicativo Node com um proxy reverso Nginx. Você também utilizou certificados SSL para proteger o domínio do seu aplicativo e configurou um trabalho cron para renovar esses certificados quando necessário.

Se estiver interessado em aprender mais sobre plug-ins do Let’s Encrypt, consulte nossos artigos sobre o uso do plug-in Nginx ou do plug-in standalone.

Você também pode aprender mais sobre o Docker Compose consultando os seguintes recursos:

A documentação do Compose também é um ótimo recurso para aprender mais sobre aplicativos multi-contêiner.

0 Comments

Creative Commons License