GuíasGuides · Nivel 3Level 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 sitioApache on Ubuntu: virtual hosts, HTTPS with certbot, security headers, logs and the default page that shows instead of your site

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.Serving several sites with Apache on Ubuntu: sites-available, a2ensite and a2enmod, ServerName and ServerAlias, the HTTP to HTTPS redirect, Let's Encrypt certificates with certbot and automatic renewal, security headers, log formats, and the site that shows Apache's default page or someone else's.

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.

What you'll get

An Apache server on Ubuntu serving several sites of its own (not as a proxy, but from their files), each with its name, its Let's Encrypt certificate renewed automatically, the redirect to HTTPS, the security headers and a log that says how long each request took. And, when someone opens their site and sees Apache2 Default Page: It works! or another department's site, knowing how to read apache2ctl -S and find the reason.

Before you start

  • Ubuntu Server 24.04 LTS with apache2 installed (sudo apt install apache2) and a user with sudo.
  • The public names in DNS pointing at the server: here reservas.example.com and portal.example.com, with their www.
  • Ports 80 and 443 open from the internet: Let's Encrypt validates on 80 (sudo ufw allow "Apache Full").
  • For internal names (*.contoso.local), Let's Encrypt does not work: the certificate is issued by the corporate CA and configured by hand as in the reverse proxy guide.

1. How Apache organises sites on Ubuntu

PathWhat it holds
/etc/apache2/apache2.confThe global configuration and the LogFormats
/etc/apache2/sites-available/One .conf file per site, enabled or not
/etc/apache2/sites-enabled/Links to enabled sites, created by a2ensite
/etc/apache2/mods-enabled/Enabled modules, with a2enmod / a2dismod
/etc/apache2/conf-enabled/Global snippets, with a2enconf (for example security.conf)
/var/www/Each site's content
/var/log/apache2/access.log, error.log and each site's own logs

Files in sites-enabled load in alphabetical order, and the order matters: the first one listening on a port is that port's default site. The one Ubuntu ships is called 000-default.conf precisely so that it comes first.

2. One virtual host per site

/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 timing</VirtualHost>
bash · enable the site
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 is the main name; ServerAlias, the others (with and without www). A name in neither is not this site's.
  • Options -Indexes stops a folder without index.html from listing its files.
  • a2ensite and a2dissite only create and remove the link: a reload is needed afterwards. The timing format is defined in section 5; until then, configtest will fail.

3. HTTPS with certbot and its renewal

bash · certbot with the Apache plugin
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 creates a second file, reservas-le-ssl.conf, with the <VirtualHost *:443> and the certificate paths. Whatever you change later (headers, logs) goes in both files: port 80's and port 443's.
  • --redirect adds a rule to the port 80 site that sends everything to HTTPS with a 301. Without certbot, the same in one line: Redirect permanent / https://reservas.example.com/.
  • Renewal: Ubuntu's package installs certbot.timer, which checks twice a day and renews whatever expires in under 30 days. With the certbot snap, the timer is snap.certbot.renew.timer. Let's Encrypt stopped sending expiry warning emails in 2025: if renewal fails, nobody tells you except your monitoring.
bash · check renewal
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. Security headers and what Apache says about itself

In the site's <VirtualHost *:443> (needs mod_headers):

reservas-le-ssl.conf · headers
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"

And in /etc/apache2/conf-available/security.conf, which Ubuntu ships enabled, so that errors do not reveal the exact Apache and OS version:

security.conf
ServerTokens ProdServerSignature OffTraceEnable Off
  • HSTS forces the browser to use HTTPS for a year. Turn it on once HTTPS works on every name: with includeSubDomains, on subdomains too. Start with a short max-age if in doubt.
  • A Content-Security-Policy protects more than all of the above, but depends on each application: agree it with whoever builds it and test first with Content-Security-Policy-Report-Only.
bash · check the headers
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. Log formats

apache2.conf already defines combined, common and vhost_combined. The combined format lacks what matters most for diagnosis: the site that answered and the time taken. A format of your own in /etc/apache2/conf-available/log-timing.conf, enabled with a2enconf log-timing:

log-timing.conf
LogFormat "%v %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\" %D" timing
  • %v is the ServerName of the virtual host that served the request: with it, the ticket in section 6 shows up in the log.
  • %D is the time in microseconds; %>s, the final status; %O, the bytes sent.
  • If there is a load balancer in front, %h will always be its IP: the user's goes in %{X-Forwarded-For}i, or with mod_remoteip configured.
  • Logs in /var/log/apache2/ are already rotated by /etc/logrotate.d/apache2 (14 days, compressed).

6. The ticket: "the site shows the default Apache page, or another site"

Tuesday, 10:15. Marketing has published portal.example.com on the same server. From outside, http://www.portal.example.com shows Apache2 Ubuntu Default Page and https://portal.example.com shows the bookings site, with a certificate warning.

Apache picks the virtual host in two steps: first by IP and port, and among those that match, by the name requested. If no ServerName or ServerAlias matches, the first one on that port answers. apache2ctl -S shows exactly that table:

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

How to read it, most to least common:

  1. 000-default is still enabled and is port 80's default server. Everything that does not match goes to it: here, www.portal.example.com, because portal.conf has no ServerAlias. Fix: add the alias and a2dissite 000-default (or keep it, but with a neutral page, never Apache's).
  2. The 443 site is missing: portal.example.com only appears on 80. On 443 nobody has that name, so the first one answers (bookings) with its certificate, and the browser warns that the name does not match. Fix: certbot --apache -d portal.example.com -d www.portal.example.com.
  3. A VirtualHost with an IP (<VirtualHost 10.20.5.10:443>) next to others with *:443: the IP one always wins for that address, even if the name does not match. All with * or all with an IP, never mixed.
  4. The site is not enabled: the file is in sites-available but does not appear in -S. It is missing a2ensite and a reload.
  5. DNS points at another server, or the browser remembers an old 301: curl -sI -H "Host: portal.example.com" http://127.0.0.1/ from the server itself tells you what Apache answers, with no DNS or cache in the way.
bash · after the fix
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

A habit that prevents this ticket: a first site of your own (000-catchall.conf) that returns a 404 or a neutral page for any unknown name, on 80 and on 443. That way a configuration mistake never shows someone else's site.

In the ticket: the apache2ctl -S output before and after, what was missing (alias, 443 site, a2ensite) and the curl check.

Next steps

Is something missing, or different in your environment? Get in touch and I'll fix it.

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.Found this useful? Every two weeks I send one practical idea for IT professionals: LinkedIn, projects, networking and hiring processes.

Apúntate y llévate las 33 ideasSign up and get the 33 ideas →