Todos os textos

Textos

Deploys um pouco mais seguros em hospedagens compartilhadas

Um deploy Laravel em hospedagem compartilhada, uma public_html cheia de coisa que não deveria estar ali e o caminho pra arrumar isso.

Eu tava mexendo num projeto de um cliente e encontrei alguns pontos de atenção no deploy. A aplicação Laravel inteira estava dentro da public_html. Funcionava, só que também deixava a pasta completa do projeto dentro da raiz pública do domínio.

E aí dá um B.O. grande de segurança.

Nesse caso, foi possível baixar pela internet tanto o database/database.sqlite quanto o storage/logs/laravel.log. Além disso, arquivos como .env.example, artisan, composer.json e composer.lock estavam acessíveis, e um info.php exibia informações do servidor.

A estrutura era mais ou menos essa:

public_html/                  ← raiz pública do domínio
├── app/
├── bootstrap/
├── config/
├── database/
│   └── database.sqlite        ← dava pra baixar
├── resources/
├── routes/
├── storage/
│   └── logs/laravel.log       ← dava pra baixar também
├── vendor/
├── wp/                       ← instalação WordPress
├── sideproject1/             ← outra aplicação Laravel inteira
├── public/                   ← esta deveria ser a raiz pública do Laravel
├── .env                      ← dependia de bloqueio no .htaccess
├── .env.example
├── artisan
├── composer.json
├── composer.lock
└── info.php

Dá pra sair bloqueando arquivo por arquivo no .htaccess, mas a correção principal é tirar esses arquivos da área pública. O Laravel já tem uma pasta pra receber as requisições do navegador: public/.

Preparando a aplicação

O processo ainda é meio arcaico, principalmente nessas hospedagens compartilhadas. O ideal mesmo seria essa parada toda estar automatizada por pipeline, mas enquanto isso não existe dá pra organizar o deploy manual.

Primeiro, gere os assets localmente ou na CI. Nesse projeto, o comando era:

npm ci
npm run production

Essa parte varia conforme o projeto e a ferramenta de build. Use o script que existe no seu package.json.

Também precisamos de uma vendor limpa, só com as dependências de produção:

composer install --no-dev --optimize-autoloader

Dá pra manter uma cópia local do projeto só pra preparar essa vendor antes de enviar os arquivos. O ambiente usado pra gerar as dependências precisa ser compatível com o PHP e as extensões da hospedagem.

No .env de produção, confira:

APP_ENV=production
APP_DEBUG=false

Separando a aplicação da parte pública

A ideia é deixar a aplicação em uma pasta fora da public_html:

/home/usuario/
├── projeto/
│   ├── app/
│   ├── bootstrap/
│   ├── config/
│   ├── database/
│   ├── resources/
│   ├── routes/
│   ├── storage/
│   ├── vendor/
│   ├── .env
│   ├── artisan
│   ├── composer.json
│   └── public/
└── public_html -> /home/usuario/projeto/public

Se a hospedagem deixa configurar a raiz pública do domínio, ela pode apontar diretamente para /home/usuario/projeto/public. Aqui, a solução foi fazer a public_html apontar pra essa pasta com um link simbólico. Isso depende de a hospedagem permitir o link e de o servidor web conseguir segui-lo.

Antes de trocar essa estrutura, vale olhar o que ficou largado na raiz do projeto. Nesse projeto tinha bastante coisa que precisava continuar acessível pelo navegador:

robots.txt
sitemap.xml
politica-sobre-cookies.pdf
favicon.ico
css/
js/
images/

Esses arquivos vão pra public/. Já .env, app/, config/, database/, storage/, vendor/ e os outros arquivos internos continuam fora dela.

Essa separação é a parte mais importante da mudança. Se algum upload precisa ser público, exponha só a pasta destinada a esses arquivos, não a storage/ inteira com logs e outros dados internos.

Enviando os arquivos e trocando a entrada do site

Nesse tipo de hospedagem, o deploy acaba sendo uma mistura de FTP/SFTP com SSH. No meu caso, o acesso SSH da Locaweb precisava ser habilitado pelo painel antes de conectar:

ssh usuario@dominio.do.ftp

Os arquivos da aplicação passam a ser enviados para /home/usuario/projeto/.

Pra um projeto novo, com a aplicação no lugar e sem uma public_html existente, o link fica assim:

cd /home/usuario
ln -s /home/usuario/projeto/public public_html

A sintaxe é ln -s ALVO NOME_DO_LINK. Ou seja, public_html é o nome do link, e a pasta public/ do projeto é o destino.

Nesse projeto tinha uma diferença: a aplicação antiga já estava inteira dentro da public_html. Então a própria pasta existente virou a pasta do projeto:

cd /home/usuario
mv public_html projeto
ln -s /home/usuario/projeto/public public_html

Esse mv não é uma etapa obrigatória de todo deploy Laravel. Ele fez sentido nessa migração, com projeto ainda inexistente. Antes de fazer uma troca dessas em produção, tenha backup e considere a interrupção entre mover a pasta e criar o link.

No final, o caminho ficou:

public_html → projeto/public

O WordPress e o sideproject1 no meio disso tudo

O projeto também tinha uma instalação WordPress em /wp e outra aplicação Laravel em /sideproject1. Essas URLs precisavam continuar funcionando.

Foram criados dois links dentro da public/:

cd /home/usuario/projeto
ln -s ../wp public/wp
ln -s ../sideproject1/public public/sideproject1

No sideproject1, a aplicação inteira continua em sideproject1/, mas o domínio só enxerga sideproject1/public/. A URL permanece /sideproject1, sem expor junto app/, config/, .env, vendor/ e companhia.

O WordPress é diferente: o link de public/wp expõe a instalação em wp/. Ele não cria a mesma separação que a pasta public/ do Laravel oferece.

Nesse caso, o wp-config.php também foi movido para um nível acima da instalação. Eu não sabia dessa possibilidade até mexer nisso. Ainda assim, o WordPress continua precisando dos próprios cuidados com configuração, permissões e arquivos acessíveis.

A estrutura final ficou assim:

/home/usuario/
├── projeto/
│   ├── app/
│   ├── config/
│   ├── database/
│   ├── storage/
│   ├── vendor/
│   ├── wp/
│   ├── wp-config.php
│   ├── sideproject1/
│   │   ├── app/
│   │   ├── .env
│   │   └── public/
│   ├── .env
│   ├── artisan
│   └── public/
│       ├── css/
│       ├── images/
│       ├── js/
│       ├── wp -> ../wp
│       ├── sideproject1 -> ../sideproject1/public
│       ├── robots.txt
│       ├── sitemap.xml
│       ├── politica-sobre-cookies.pdf
│       ├── index.php
│       └── .htaccess
└── public_html -> /home/usuario/projeto/public

E o .htaccess?

Com a mudança, o .htaccess da entrada do Laravel passa a ser o que está em projeto/public/.htaccess.

Antes, a raiz do projeto era a própria public_html. Agora o Apache recebe as requisições pela pasta public/, então é ali que as regras de entrada do Laravel precisam estar.

Vale revisar as regras antigas durante a troca, principalmente se elas assumiam que o site era servido pela raiz do projeto.

Testando o que abre e o que parou de abrir

Depois da mudança, comecei pelas URLs que precisavam continuar funcionando:

/wp/
/sideproject1/
/admin/
/cliente/login
/anunciante/login
/robots.txt
/sitemap.xml
/politica-sobre-cookies.pdf

Também entram nessa conferência CSS, JavaScript, imagens, uploads, pagamentos e callbacks externos. A página inicial abrir não significa que o deploy inteiro deu certo.

E tem a outra metade do teste: tentar acessar o que não deveria estar disponível.

/.env
/.env.example
/artisan
/composer.json
/composer.lock
/database/database.sqlite
/storage/logs/laravel.log
/phpunit.xml
/info.php

O esperado é não conseguir mais baixar essa penca de arquivo nem abrir a página de diagnóstico. Confira o conteúdo da resposta também: só olhar o status HTTP não basta pra saber o que o servidor entregou.

Tirar os arquivos do ar resolve a exposição dali pra frente. Não desfaz os downloads que podem ter acontecido antes, então banco e logs expostos também precisam ser tratados como um incidente, com revisão dos dados e de possíveis credenciais comprometidas.

O deploy continua manual, mas agora a estrutura acompanha o que o Laravel espera: o navegador chega na public/, e os arquivos internos ficam fora da raiz pública. Era essa a mudança que faltava aqui.