Guías · Nivel 3

Apache en Ubuntu: virtual hosts, HTTPS con certbot, cabeceras de seguridad, logs y la página por defecto que aparece en lugar de tu sitio

Servir varios sitios con Apache en Ubuntu: sites-available, a2ensite y a2enmod, ServerName y ServerAlias, la redirección de HTTP a HTTPS, certificados de Let's Encrypt con certbot y su renovación automática, cabeceras de seguridad, formatos de log, y el sitio que muestra la página por defecto de Apache o la web de otro.

Qué vas a conseguir

Un servidor Apache en Ubuntu que sirve varios sitios propios (no como proxy, sino con sus ficheros), cada uno con su nombre, su certificado de Let's Encrypt renovado solo, la redirección a HTTPS, las cabeceras de seguridad y un log que dice cuánto tarda cada petición. Y, cuando alguien abre su web y ve Apache2 Default Page: It works! o la web de otro departamento, saber leer apache2ctl -S y encontrar el motivo.

Antes de empezar

  • Ubuntu Server 24.04 LTS con apache2 instalado (sudo apt install apache2) y un usuario con sudo.
  • Los nombres públicos en DNS apuntando al servidor: aquí reservas.example.com y portal.example.com, con sus www.
  • Los puertos 80 y 443 abiertos desde internet: Let's Encrypt valida por el 80 (sudo ufw allow "Apache Full").
  • Para nombres internos (*.contoso.local), Let's Encrypt no sirve: el certificado lo emite la CA corporativa y se configura a mano como en la guía de proxy inverso.

1. Cómo organiza Apache los sitios en Ubuntu

RutaQué contiene
/etc/apache2/apache2.confLa configuración global y los LogFormat
/etc/apache2/sites-available/Un fichero .conf por sitio, activo o no
/etc/apache2/sites-enabled/Enlaces a los sitios activos, creados por a2ensite
/etc/apache2/mods-enabled/Módulos activos, con a2enmod / a2dismod
/etc/apache2/conf-enabled/Fragmentos globales, con a2enconf (por ejemplo security.conf)
/var/www/El contenido de cada sitio
/var/log/apache2/access.log, error.log y los logs propios de cada sitio

Los ficheros de sites-enabled se cargan por orden alfabético, y ese orden importa: el primero que escucha en un puerto es el sitio por defecto de ese puerto. El que trae Ubuntu se llama 000-default.conf precisamente para ir el primero.

2. Un virtual host por sitio

/etc/apache2/sites-available/reservas.conf
<VirtualHost *:80>    ServerName reservas.example.com    ServerAlias www.reservas.example.com    DocumentRoot /var/www/reservas    <Directory /var/www/reservas>        Options -Indexes        AllowOverride None        Require all granted    </Directory>    ErrorLog ${APACHE_LOG_DIR}/reservas_error.log    CustomLog ${APACHE_LOG_DIR}/reservas_access.log tiempos</VirtualHost>
bash · activar el sitio
sudo mkdir -p /var/www/reservas && sudo chown -R www-data:www-data /var/www/reservassudo a2ensite reservassudo a2dissite 000-defaultsudo apache2ctl configtestSyntax OKsudo systemctl reload apache2
  • ServerName es el nombre principal; ServerAlias, los demás (con www y sin él). Un nombre que no está en ninguno de los dos no es de este sitio.
  • Options -Indexes evita que una carpeta sin index.html muestre la lista de ficheros.
  • a2ensite y a2dissite solo crean y quitan el enlace: hace falta reload después. El formato tiempos se define en el apartado 5; hasta entonces, configtest dará error.

3. HTTPS con certbot y su renovación

bash · certbot con el plugin de Apache
sudo apt install certbot python3-certbot-apachesudo a2enmod ssl headers rewritesudo certbot --apache -d reservas.example.com -d www.reservas.example.com --redirect -m it@example.com --agree-tosSuccessfully received certificate.Certificate is saved at: /etc/letsencrypt/live/reservas.example.com/fullchain.pemKey is saved at:         /etc/letsencrypt/live/reservas.example.com/privkey.pemSuccessfully deployed certificate for reservas.example.com to /etc/apache2/sites-available/reservas-le-ssl.confCongratulations! You have successfully enabled HTTPS on https://reservas.example.com and https://www.reservas.example.com
  • certbot crea un segundo fichero, reservas-le-ssl.conf, con el <VirtualHost *:443> y las rutas del certificado. Lo que cambies después (cabeceras, logs) va en los dos ficheros: el del 80 y el del 443.
  • --redirect añade al sitio del puerto 80 una regla que manda todo a HTTPS con un 301. Sin certbot, lo mismo en una línea: Redirect permanent / https://reservas.example.com/.
  • La renovación: el paquete de Ubuntu instala certbot.timer, que comprueba dos veces al día y renueva lo que caduca en menos de 30 días. Con el snap de certbot, el temporizador es snap.certbot.renew.timer. Let's Encrypt dejó de enviar correos de aviso de caducidad en 2025: si la renovación falla, nadie te avisa salvo tu monitorización.
bash · comprobar la renovación
systemctl list-timers certbot.timersudo certbot certificatessudo certbot renew --dry-runCongratulations, all simulated renewals succeeded:  /etc/letsencrypt/live/reservas.example.com/fullchain.pem (success)

4. Cabeceras de seguridad y lo que Apache cuenta de sí mismo

En el <VirtualHost *:443> del sitio (necesita mod_headers):

reservas-le-ssl.conf · cabeceras
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"Header always set X-Content-Type-Options "nosniff"Header always set X-Frame-Options "SAMEORIGIN"Header always set Referrer-Policy "strict-origin-when-cross-origin"

Y en /etc/apache2/conf-available/security.conf, que Ubuntu trae activo, para que los errores no digan la versión exacta de Apache y del sistema:

security.conf
ServerTokens ProdServerSignature OffTraceEnable Off
  • HSTS obliga al navegador a usar HTTPS durante un año. Actívalo cuando HTTPS funcione bien en todos los nombres: con includeSubDomains, también en los subdominios. Empieza con un max-age corto si tienes dudas.
  • Una Content-Security-Policy protege más que todas las anteriores, pero depende de cada aplicación: se acuerda con quien la desarrolla y se prueba antes con Content-Security-Policy-Report-Only.
bash · comprobar las cabeceras
curl -sI https://reservas.example.com | grep -iE "^(server|strict|x-content|x-frame|referrer)"Server: ApacheStrict-Transport-Security: max-age=31536000; includeSubDomainsX-Content-Type-Options: nosniffX-Frame-Options: SAMEORIGINReferrer-Policy: strict-origin-when-cross-origin

5. Formatos de log

apache2.conf ya define combined, common y vhost_combined. Al formato combinado le falta lo más útil para diagnosticar: el nombre del sitio que respondió y el tiempo. Un formato propio en /etc/apache2/conf-available/log-tiempos.conf, activado con a2enconf log-tiempos:

log-tiempos.conf
LogFormat "%v %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\" %D" tiempos
  • %v es el ServerName del virtual host que atendió la petición: con él, el ticket del apartado 6 se ve en el log.
  • %D es el tiempo en microsegundos; %>s, el código final; %O, los bytes enviados.
  • Si delante hay un balanceador, %h será siempre su IP: la del usuario va en %{X-Forwarded-For}i, o con mod_remoteip configurado.
  • Los logs de /var/log/apache2/ ya los rota /etc/logrotate.d/apache2 (14 días, comprimidos).

6. El ticket: "la web muestra la página por defecto de Apache, o la de otro sitio"

Martes, 10:15. Marketing ha publicado portal.example.com en el mismo servidor. Desde fuera, http://www.portal.example.com muestra Apache2 Ubuntu Default Page y https://portal.example.com enseña la web de reservas, con un aviso de certificado.

Apache elige el virtual host en dos pasos: primero por IP y puerto, y entre los que coinciden, por el nombre pedido. Si ningún ServerName ni ServerAlias coincide, responde el primero de ese puerto. apache2ctl -S enseña exactamente esa tabla:

bash · apache2ctl -S
sudo apache2ctl -SVirtualHost configuration:*:443                  is a NameVirtualHost         default server reservas.example.com (/etc/apache2/sites-enabled/reservas-le-ssl.conf:2)         port 443 namevhost reservas.example.com (/etc/apache2/sites-enabled/reservas-le-ssl.conf:2)                 alias www.reservas.example.com*:80                   is a NameVirtualHost         default server srv-web-01.contoso.local (/etc/apache2/sites-enabled/000-default.conf:1)         port 80 namevhost srv-web-01.contoso.local (/etc/apache2/sites-enabled/000-default.conf:1)         port 80 namevhost portal.example.com (/etc/apache2/sites-enabled/portal.conf:1)         port 80 namevhost reservas.example.com (/etc/apache2/sites-enabled/reservas.conf:1)                 alias www.reservas.example.com

Cómo se lee, de más a menos frecuente:

  1. 000-default sigue activo y es el default server del 80. A él va todo lo que no coincide: aquí, www.portal.example.com, porque portal.conf no tiene ServerAlias. Solución: añadir el alias y a2dissite 000-default (o dejarlo, pero con una página neutra, nunca la de Apache).
  2. Falta el sitio del 443: portal.example.com solo aparece en el 80. En el 443 no hay nadie con ese nombre, así que responde el primero (reservas) con su certificado, y el navegador avisa de que el nombre no coincide. Solución: certbot --apache -d portal.example.com -d www.portal.example.com.
  3. Un VirtualHost con IP (<VirtualHost 10.20.5.10:443>) junto a otros con *:443: el de la IP gana siempre para esa dirección, aunque el nombre no coincida. Todos con * o todos con IP, no mezclados.
  4. El sitio no está activo: el fichero está en sites-available pero no aparece en -S. Falta a2ensite y reload.
  5. El DNS apunta a otro servidor, o el navegador recuerda un 301 antiguo: curl -sI -H "Host: portal.example.com" http://127.0.0.1/ desde el propio servidor dice qué responde Apache, sin DNS ni caché de por medio.
bash · después de corregir
sudo apache2ctl configtest && sudo systemctl reload apache2curl -sI https://www.portal.example.com | head -n 1HTTP/1.1 200 OKsudo tail -n 1 /var/log/apache2/portal_access.logportal.example.com 203.0.113.24 - - [03/Nov/2026:10:31:07 +0100] "GET / HTTP/1.1" 200 5120 "-" "Mozilla/5.0" 1840

Una costumbre que evita este ticket: un primer sitio propio (000-catchall.conf) que devuelve un 404 o una página neutra para cualquier nombre desconocido, en el 80 y en el 443. Así un error de configuración nunca enseña la web de otro.

En el ticket: la salida de apache2ctl -S antes y después, qué faltaba (alias, sitio del 443, a2ensite) y la comprobación con curl.

Siguientes pasos

¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.

Tech Career Lab · newsletter

¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.

Apúntate y llévate las 33 ideas →