Seguridad, endurecimiento y puesta en producción
Seguridad, endurecimiento y puesta en producción
En el capítulo 15 terminaste de personalizar la cara visible del sistema. Ahora queda la parte que nadie ve hasta que falla: dejar la instalación en un estado en el que puedas cobrarle dinero a gente real sin que un fin de semana malo se convierta en un incidente de datos.
Este capítulo empieza por lo incómodo. FOSSBilling 0.8.5 es software declarado beta que gestiona facturas, tarjetas de crédito por intermediación y credenciales de servidores de hosting. Ha publicado 32 advisories de seguridad, todos durante 2026, cuatro de ellos críticos. Y no tiene autenticación de dos factores. Ninguna. Esas tres frases no son un argumento para no usarlo: son el punto de partida para operarlo con la disciplina que exige.
Al terminar tendrás las capas de defensa ordenadas de la red hacia los datos, backups verificados con prueba de restauración, rotación de logs que no llena el disco, un procedimiento de actualización que no deja el cron parado y una lista de comprobaciones para ejecutar antes de abrir el registro público.
El punto de partida honesto: beta declarada, 32 advisories y 0.8.5 como baseline
El proyecto declara su estado en tres sitios distintos y conviene leerlos literalmente. El README del repositorio lleva un aviso destacado: “FOSSBilling is under active development and currently considered beta. Expect rough edges and limited support.” La página de descargas dice “FOSSBilling is currently pre-production software”. Y la FAQ oficial suaviza el tono sin quitar la advertencia: “It is already usable, but you should treat production deployments with the same care you would apply to any billing system: keep backups, test updates in staging, and avoid running unreleased code unless you understand the risk.”
Sobre eso se apoya la declaración más importante del proyecto, publicada verbatim en las notas de release de 0.8.5:
“Alongside this release, we are publishing several security advisories for vulnerabilities addressed in 0.8.4 and later. Users should upgrade as soon as possible as prior releases contain critical vulnerabilities. 0.8.5 should be considered the recommended security baseline.”
Los números detrás de esa frase, consultados contra la API de GitHub Security Advisories: 32 advisories en total, 4 críticos, 12 altos, 11 medios y 5 bajos. Y dos datos que cambian por completo la lectura. Uno. Los 32 se publicaron en 2026; no hay ni un solo advisory anterior. Dos. Veintiséis de los 32 se corrigieron en 0.8.0, publicada el 28 de mayo de 2026. Ese release no fue una versión más: fue una auditoría masiva del código heredado de BoxBilling, que llevaba sin mantenimiento desde 2022.
La ausencia total de advisories antes de 2026 no significa que el código fuera seguro. Significa que nadie estaba mirando. Publicar 32 CVEs con rangos de versión precisos y versión parcheada es un acto de madurez del proyecto, no de fracaso. Pero también significa que toda la base instalada anterior a 0.8.0 es explotable con información pública. El propio proyecto lo considera tan grave que empuja una alerta central no descartable a todas las instancias afectadas:
curl -sS https://fossbilling.org/api/central-alerts/list
{
"title": "This version of FOSSBilling is insecure",
"message": "FOSSBilling versions older than v0.8.0 contain critical security vulnerabilities that allow attackers to take direct control of your server.",
"type": "danger",
"dismissible": false,
"max_fossbilling_version": "0.7.2"
}
"dismissible": false. El aviso no se puede cerrar.
Cómo leer un advisory y decidir si te afecta
Un advisory trae cuatro campos que determinan tu exposición: rango de versiones vulnerables, versión parcheada, severidad y vector. La pregunta operativa no es “¿es grave?” sino “¿mi versión cae dentro del rango y el vector es alcanzable desde donde estoy expuesto?”. Empieza por saber qué corres y comparar contra la última publicada:
LOCAL=$(sudo -u www-data php /var/www/fossbilling/console.php system:version | tr -dc '0-9.')
LATEST=$(curl -fsSL https://api.github.com/repos/FOSSBilling/FOSSBilling/releases/latest \
| grep -oP '"tag_name":\s*"\K[^"]+')
echo "instalada=$LOCAL ultima=$LATEST"
[ "$LOCAL" = "$LATEST" ] && echo "AL DIA" || echo "DESACTUALIZADA - revisar advisories"
El árbol de decisión completo, con la acción concreta en cada rama:
flowchart TD
A["Que version corres"] --> B{"Comparar con 0.8.0"}
B -->|"menor que 0.8.0"| C["Cuatro criticos activos"]
C --> C2["Bypass de autenticacion y RCE por SSTI publicos"]
C2 --> C3["Actualizar YA a 0.8.5 y asumir posible compromiso"]
B -->|"0.8.0 a 0.8.3"| D["Faltan los parches de 0.8.4"]
D --> D2["CVE-2026-62962 CVE-2026-68542 CVE-2026-68543"]
D2 --> D3["Actualizar a 0.8.5 en la ventana de mantenimiento"]
B -->|"0.8.4"| E["Faltan CVE-2026-68544 y CVE-2026-68545"]
E --> E2["Sesiones que sobreviven al reset de password"]
E2 --> E3["Actualizar a 0.8.5"]
B -->|"0.8.5"| F["Baseline recomendado por el proyecto"]
F --> F2["Vigilar advisories y releases del repositorio"]
Para no enterarte tarde, suscríbete a las notificaciones del repositorio con gh api -X PUT repos/FOSSBilling/FOSSBilling/subscription -f subscribed=true, o en la interfaz web con Watch → Custom → Releases + Security alerts. El listado completo vive en https://github.com/FOSSBilling/FOSSBilling/security/advisories.
Los cuatro críticos y los altos posteriores a 0.8.0
Los cuatro críticos comparten dos características: todos se parchearon en 0.8.0 y todos afectan a ramas 0.7.x o anteriores.
| CVE | GHSA | Resumen | Versiones afectadas |
|---|---|---|---|
| CVE-2026-33543 | GHSA-28mh-j262-q49w | Bypass de autenticación que permite crear un administrador sin autenticar | <= 0.7.2 |
| CVE-2026-28496 | GHSA-57mv-jm88-66jc | Server-side template injection en el renderizado Twig, con divulgación de información y RCE | <= 0.7.2 |
| CVE-2026-27604 | GHSA-78x5-c8gw-8279 | Validación incorrecta del rol system en la API, acceso no autenticado a funciones privilegiadas | >= 0.5.4, < 0.8.0 |
| CVE-2026-42341 | GHSA-5493-9m76-2qrr | Bypass de pago no autenticado por forja de callback IPN | >= 0.6.0, <= 0.7.2 |
Léelos en conjunto: creación de administrador sin credenciales, ejecución remota de código y marcado de facturas como pagadas sin pagar. Correr 0.7.2 hoy no es “usar una versión antigua”: es exponer un panel de facturación con toma de control remota conocida y pública. Ahora la parte que mucha gente pasa por alto: actualizar a 0.8.0 no basta, porque entre 0.8.0 y 0.8.5 hubo cinco releases más con contenido de seguridad.
| CVE | Resumen | Afectadas | Parcheado en |
|---|---|---|---|
| CVE-2026-68544 | El reset de contraseña por los endpoints de confirmación de invitado no revoca las sesiones existentes | >= 0.5.5, <= 0.8.4 | 0.8.5 |
| CVE-2026-68545 | Los pedidos de hosting expirados siguen gestionables por la client API hasta que el cron los suspende | >= 0.1.0, <= 0.8.4 | 0.8.5 |
| CVE-2026-62962 | Autorización ausente e incompleta en los endpoints de configuración de extensiones | >= 0.1.0, <= 0.8.3 | 0.8.4 |
| CVE-2026-68542 | El adaptador IPN de PayPalEmail no verifica el destinatario: forja de pagos no autenticada | >= 0.1.0, <= 0.8.3 | 0.8.4 |
| CVE-2026-68543 | El precio no se valida al crear pedidos desde el panel: aprovisionamiento sin pago | >= 0.1.0, <= 0.8.3 | 0.8.4 |
| CVE-2026-53648 | Los ficheros de productos descargables se pueden sobrescribir por colisión de nombre | >= 0.1.0, <= 0.8.0 | 0.8.1 |
Gotcha: CVE-2026-68544 es el que más duele en una instalación viva. Si un atacante consiguió sesión en algún momento y tú reaccionaste cambiando la contraseña del cliente afectado, en <= 0.8.4 esa sesión seguía siendo válida. El reset no era una medida de contención. Solo lo es desde 0.8.5.
Dos patrones se repiten en toda la lista y valen como brújula de riesgo. El primero es la autorización: IDOR, comprobaciones de rol ausentes, endpoints de invitado que exponen datos de otros clientes. El segundo son las pasarelas de pago: cuatro advisories tocan IPN y PayPal, dos de ellos permiten falsificar pagos. Es un argumento técnico directo a favor de Stripe con secreto de firma de webhook, que es obligatorio desde 0.8.4, frente a PayPalEmail con IPN. Ese contraste lo desarrollaste en el capítulo 10.
No hay 2FA en 0.8.5: el hallazgo y cuatro mitigaciones de infraestructura
Esto hay que decirlo sin rodeos porque es la carencia más relevante del producto. FOSSBilling 0.8.5 no tiene autenticación de dos factores. La verificación es exhaustiva: cero coincidencias de 2fa, twofactor, two_factor, totp y otp en las 1.598 rutas del tag 0.8.5; cero páginas sobre 2FA en los 54 ficheros de la documentación oficial; el issue #62 «Multi-factor Authentication» sigue abierto y el PR #2159 «Add Support for 2FA for “Admin”» está cerrado sin fusionar. El issue #62 está asignado al milestone 0.9.x, así que no es algo que llegue en un parche.
La página oficial de mejores prácticas, en su sección Admin Access, recomienda exactamente tres cosas y ninguna es 2FA: “Use strong, unique passwords for admin accounts”, “Consider restricting admin panel access by IP if possible” y “Regularly review admin activity logs”.
Como no hay solución en la aplicación, la mitigación es de infraestructura. Cuatro opciones, de menos a más completa. Uno. Restringir el panel por IP en nginx, la más simple y la más eficaz si trabajas desde ubicaciones fijas:
location ^~ /admin {
allow 203.0.113.0/24; # rango de la oficina
allow 198.51.100.42; # salida de la VPN
deny all;
try_files $uri $uri/ @rewrite;
}
Dos. Autenticación HTTP básica delante del panel. Añade un factor de conocimiento adicional antes de que la petición llegue siquiera a PHP. Genera el fichero con sudo htpasswd -c /etc/nginx/.htpasswd-fossbilling admin del paquete apache2-utils, déjalo en 640 y con grupo www-data:
location ^~ /admin {
auth_basic "Restringido";
auth_basic_user_file /etc/nginx/.htpasswd-fossbilling;
try_files $uri $uri/ @rewrite;
}
Advertencia: aplica esto solo a las rutas de interfaz del panel, nunca a /api/. El .htaccess incluido propaga deliberadamente la cabecera Authorization a PHP con RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}], porque la API la usa para autenticar con API key mediante Basic Auth. Si pones un auth_basic encima de /api/, los clientes de API dejan de autenticarse. El detalle de cómo funciona esa autenticación está en el capítulo 14.
Tres. Un proxy con SSO y 2FA delante. Authelia, Authentik, Cloudflare Access u oauth2-proxy resuelven el problema de verdad: exigen segundo factor antes de dejar pasar la petición. Es la opción más completa. No existe documentación oficial de FOSSBilling sobre integración con ningún proveedor SSO, así que se trata de una recomendación de infraestructura general, no de una guía soportada por el proyecto.
Cuatro. Panel accesible solo por VPN. WireGuard o Tailscale, combinado con la restricción por IP de la opción uno. Elimina la superficie pública del panel por completo.
Elige según el modelo de operación: un solo operador con IP fija, restricción por IP; varios operadores en ubicaciones cambiantes, proxy con SSO y 2FA; equipo pequeño con VPN ya montada, VPN más restricción por IP; y si necesitas algo hoy sin infraestructura extra, Basic Auth solo sobre las rutas de interfaz.
HTTPS en dos capas y el bucle de redirección detrás de un proxy
La postura oficial en security/best-practices.mdoc es literal: “Always use HTTPS in production”, “Set force_https to true in your FOSSBilling config” y “Use valid SSL certificates (Let’s Encrypt is free and easy)”.
Hay dos capas y necesitas las dos, porque hacen cosas distintas. Capa del servidor web: el bloque del puerto 80 con redirección permanente, dejando pasar antes el reto ACME de Let’s Encrypt:
server {
listen 80;
listen [::]:80;
server_name billing.example.com;
location ^~ /.well-known/acme-challenge/ { root /var/www/html; allow all; }
location / { return 301 https://$host$request_uri; }
}
Capa de la aplicación. security.force_https => true en config.php, implementado en load.php::checkSSL():
function checkSSL(): void
{
global $request;
if (Config::getProperty('security.force_https') && !Environment::isCLI()) {
if (!$request->isSecure()) {
emitResponse(new RedirectResponse('https://' . $request->getHost() . $request->getRequestUri()));
}
}
}
La redirección de esta segunda capa es lo de menos. Lo que realmente importa es que force_https marca las cookies como seguras, y eso es lo que protege la sesión de un downgrade; la primera capa sola no te da eso.
Gotcha: el bucle de redirección infinito detrás de un proxy inverso. $request->isSecure() depende de que Symfony confíe en la cabecera X-Forwarded-Proto. Sin security.trusted_proxies configurado, FOSSBilling cree que la petición llegó por HTTP, redirige a HTTPS, el proxy la reenvía otra vez como HTTP internamente y el ciclo se repite hasta que el navegador corta. El síntoma es inequívoco y la causa es siempre esta.
'security' => [
'force_https' => true,
'trusted_proxies' => [
'enabled' => true,
'proxies' => ['10.0.0.0/8', '172.16.0.0/12'],
'headers' => 'x_forwarded',
],
],
La documentación oficial lo dice sin ambigüedad: “Reverse proxies often make FOSSBilling think it is being accessed over HTTP even when the visitor is using HTTPS. To avoid that, make sure your proxy forwards X-Forwarded-Proto: https.” Y la guía de instalación pide reenviar también X-Forwarded-Host.
El campo headers acepta x_forwarded y forwarded según la documentación; el instalador además admite aws_elb y traefik. Repasaste esa discrepancia y el resto de claves en el capítulo 3.
Cabeceras de seguridad: lo que el proyecto no publica, y SRI contra Cloudflare
Aquí hay un hallazgo negativo que conviene declarar antes de copiar nada: la documentación oficial de FOSSBilling no publica un conjunto recomendado de cabeceras de seguridad. Ni security/securing-fossbilling.mdoc, ni security/best-practices.mdoc, ni la plantilla de nginx de la guía de instalación contienen Strict-Transport-Security, X-Frame-Options ni Content-Security-Policy. Cualquier conjunto que apliques es aportación tuya como operador. Un punto de partida razonable, presentado como lo que es —buena práctica general, no recomendación del proyecto—:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
Lo que sí está verificado y documentado es una interacción que rompe el panel. El panel de administración usa Subresource Integrity con hashes sha384. La documentación de troubleshooting recoge el fallo literal:
“Enabling Cloudflare’s ‘Auto Minify’ feature can break integrity checks in the admin panel. When that happens, JavaScript and CSS files stop loading and the admin panel may become unusable. A browser error usually looks like this:
None of the "sha384" hashes in the integrity attribute match the content of the subresource.Disable ‘Auto Minify’ to fix the issue.”
El corolario es más amplio que Cloudflare: cualquier cosa que reescriba los assets en tránsito rompe el panel. Minificadores de CDN, proxies que modifican HTML, algunos WAF con optimización agresiva. Si el panel se queda sin estilos y la consola del navegador menciona sha384, no busques en PHP: busca en el borde.
De Cloudflare sí hay una recomendación oficial positiva: “If you’re using Cloudflare, enable IP Geolocation under your site’s Network settings. This allows FOSSBilling to use the visitor’s country information to strengthen some session checks.”
Cerrar la superficie: /install, /data, /vendor, config.old.php y themes/*/config
La plantilla oficial de nginx bloquea /data/, /vendor/, /config.php, los ficheros ocultos salvo /.well-known/ y las extensiones sensibles. Tiene tres huecos verificados frente al .htaccess de Apache:
| Ruta | .htaccess | Plantilla nginx |
|---|---|---|
/data/, /vendor/, /config.php | Sí | Sí |
/config.old.php | Sí, por la extensión .old y .php | No |
/themes/*/config/ | Sí, regla explícita | No |
/install/ | Allowlist limitada | No |
config.old.php no es hipotético: FOSSBilling\Config crea ese respaldo automáticamente cada vez que la aplicación actualiza la configuración, y contiene el mismo salt y las mismas credenciales de base de datos que config.php. El bloque completo, con el orden correcto —^~ y = ganan a los regex—:
location ^~ /data/ { return 403; }
location ^~ /vendor/ { return 403; }
location ^~ /install/ { return 403; }
location = /config.php { return 403; }
location = /config.old.php { return 403; }
location ~ ^/themes/[^/]+/config/ { return 403; }
location ~* \.(ini|sh|inc|bak|twig|sql|log|yaml|yml|lock)$ { return 403; }
location ~ /\.(?!well-known\/) { return 403; }
No bloquees /public/. Desde 0.8.0 ahí viven los assets compartidos: /public/assets, /public/gateways y /public/branding. La guía de migración 0.7 a 0.8 lo dice explícitamente: “make sure /public remains publicly readable while /data remains blocked”. Sobre /install: en producción, en la primera petición tras instalar, load.php::checkInstaller() borra la carpeta automáticamente si existe config.php. Ese borrado depende de que el usuario del servidor web tenga permiso para eliminar el directorio, y si no lo tiene, la carpeta se queda sin aviso llamativo. Verificalo siempre a mano:
test -d /var/www/fossbilling/install && echo "PELIGRO: /install sigue ahi" || echo "OK: /install borrado"
# Si sigue ahi: rm -rfv /var/www/fossbilling/install
for p in install/install.php config.php config.old.php data/log/ vendor/autoload.php; do
printf '%-24s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' https://billing.example.com/$p)"
done
# Los cinco deben devolver 403 o 404
curl -s -o /dev/null -w "%{http_code}\n" https://billing.example.com/public/branding/ # NO debe ser 403
Sacar path_data fuera de la raíz web
Las reglas del servidor web son una defensa condicional: dependen de que la regla exista, esté bien escrita y no la pise otra directiva. La defensa estructural es distinta: si el directorio no está bajo la raíz de documentos, no hay regla que pueda fallar. path_data controla dónde viven caché, logs y uploads, y por defecto apunta a __DIR__ . '/data'. De su valor se derivan en load.php las constantes PATH_DATA, PATH_CACHE = PATH_DATA/cache y PATH_LOG = PATH_DATA/log.
// config.php
'path_data' => '/var/lib/fossbilling/data',
sudo mkdir -p /var/lib/fossbilling/data/{cache,log,uploads}
sudo rsync -a /var/www/fossbilling/data/ /var/lib/fossbilling/data/
sudo chown -R www-data:www-data /var/lib/fossbilling
sudo chmod -R 750 /var/lib/fossbilling
sudo -u www-data php /var/www/fossbilling/console.php cache:clear
Gotcha: twig.cache no sigue a path_data. Es una clave independiente que apunta por defecto a __DIR__ . '/data/cache'. Si mueves path_data y te olvidas de esto, Twig seguirá compilando plantillas dentro de la raíz web. Cámbiala a 'cache' => '/var/lib/fossbilling/data/cache' en el mismo config.php.
Nota de honestidad: el proyecto no documenta este escenario. El parámetro existe y es configurable, pero no hay guía oficial de path_data fuera de la raíz web ni de sus efectos secundarios. Comprueba tras el cambio que la aplicación escribe donde esperas, ejecutando cron.php a mano y mirando ls -la /var/lib/fossbilling/data/log/cron/.
Cambiar admin_area_prefix: ofuscación, no seguridad
El prefijo del área de administración es /admin por defecto. Se cambia con una clave de nivel superior de config.php —'admin_area_prefix' => '/gestion-interna-7fa2',— y hay que limpiar la caché después con console.php cache:clear, o el cambio no se aplica.
El valor se expone como constante ADMIN_PREFIX en load.php, y index.php la usa para decidir a qué aplicación enruta la petición:
if (strncasecmp($url, ADMIN_PREFIX, strlen(ADMIN_PREFIX)) === 0) {
define('ADMIN_AREA', true);
$app = new Box_AppAdmin([], $debugBar);
} else {
define('ADMIN_AREA', false);
$app = new Box_AppClient([], $debugBar);
}
Dicho claro: esto es ofuscación, no seguridad. Reduce el ruido de escáneres automáticos que prueban /admin a ciegas, y por tanto reduce el volumen de intentos que llegan al rate limiter. No detiene a nadie que te tenga en el punto de mira. Su valor real es combinado: menos ruido en los logs significa que los intentos que quedan son más informativos. Dos detalles prácticos. Si cambias el prefijo, actualiza también cualquier location ^~ /admin de nginx que uses para restringir por IP, o dejarás el panel abierto en su nueva ruta. Y recuerda que el instalador construye la URL de la página de éxito como SYSTEM_URL . 'admin', así que ese enlace apuntará a /admin aunque después cambies el prefijo.
Rate limiting en producción y su dependencia de trusted_proxies
El limitador está activado por defecto con 30 políticas predefinidas, apoyado en el componente Symfony RateLimiter. Un bloque rate_limiter con policies vacío significa “usa los valores por defecto de FOSSBilling\Security\RateLimiter::getDefaultConfig()”, no “sin límites”.
Las políticas que importan desde el punto de vista de seguridad:
| Política | Tipo | Límite | Intervalo | Para qué |
|---|---|---|---|---|
api_login | fixed_window | 10 | 1 hora | Fuerza bruta contra el login |
staff_password_reset_ip | fixed_window | 5 | 1 hora | Abuso del reset de contraseña de staff |
staff_password_reset_email | fixed_window | 3 | 1 hora | Bombardeo a una cuenta concreta |
client_signup | fixed_window | 5 | 1 hora | Registro masivo de cuentas |
guest_ticket_create | fixed_window | 3 | 1 hora | Spam por el formulario de tickets |
api_guest | token_bucket | 100 | 60 segundos | Scraping de endpoints públicos |
api_authenticated_ip | token_bucket | 1000 | 1 hora | Integraciones desbocadas |
Gotcha de primer orden: consume() resuelve la IP con $this->di['request']->getClientIp(). Detrás de un proxy inverso sin trusted_proxies configurado, todas las peticiones comparten la IP del proxy. El efecto es doble y ambos lados son malos: los diez intentos por hora de api_login se agotan con el primer bot y bloqueas a todos tus clientes legítimos, mientras que el atacante real solo necesita esperar. El rate limiter y la configuración de proxies no son dos temas: son el mismo tema.
Desde 0.8.4 hay interfaz de gestión de límites en el panel, así que puedes revisar el estado sin editar config.php. Para sobrescribir una política concreta:
'rate_limiter' => [
'enabled' => true,
'whitelist_ips' => ['203.0.113.10'],
'policies' => [
'client_signup' => ['policy' => 'fixed_window', 'limit' => 5, 'interval' => '1 hour'],
],
],
whitelist_ips acepta IPs y CIDRs, y está pensado para integraciones con IP fija que hacen muchas llamadas a la API. Úsalo con cuentagotas: cada IP en esa lista es una IP sin ninguna protección de tasa. Cuando se supera un límite, la API devuelve código de aplicación 429 con HTTP 429 y añade la cabecera Retry-After con los segundos restantes.
Antispam: Turnstile, hCaptcha, honeypot y reCAPTCHA v3
El módulo Antispam sustituyó a Spamchecker en 0.8.0. Según security/securing-fossbilling.mdoc ofrece “CAPTCHA, IP blocking, disposable email detection, Stop Forum Spam lookups, and honeypot fields”:
| Mecanismo | Qué hace | Cuándo usarlo |
|---|---|---|
| Cloudflare Turnstile | Reto sin interacción, sin cookies de tracking | Opción por defecto si ya usas Cloudflare |
| hCaptcha | Reto clásico con proveedor alternativo | Si prefieres no depender de Cloudflare |
| reCAPTCHA v3 | Puntuación por comportamiento, sin reto visible. Añadido en 0.8.1 | Cuando el reto visible daña la conversión |
| Honeypot | Campo oculto que solo rellenan los bots | Siempre. Cuesta cero y filtra bots tontos |
| Bloqueo de IP | Lista de IPs vetadas | Reacción a un abuso concreto |
| Email desechable | Detección de dominios de correo temporal | Registro público abierto |
| Stop Forum Spam | Consulta a una base de datos colaborativa | Registro público con volumen |
El honeypot y el rate limiter cubren capas distintas y se complementan: el honeypot filtra por comportamiento del cliente, el limitador por volumen desde un origen. Activa siempre el honeypot y elige un proveedor de CAPTCHA; encadenar dos no suma seguridad y sí resta conversión en el formulario de registro que montaste en el capítulo 6.
Endurecer el servidor: ufw, MariaDB en loopback, SSH, unattended-upgrades y fail2ban
Las recomendaciones oficiales de security/best-practices.mdoc, literales: “Don’t run services as root — use sudo when needed”, “Use SSH keys instead of passwords for server access”, “Disable root login over SSH”, “Use a firewall to close unnecessary ports”, “Don’t expose your database to the internet unless absolutely necessary”, “Limit database user privileges to what’s required”, “Keep mode set to strict in your security config”, “Don’t increase session_lifespan unnecessarily” y “Apply operating system security patches promptly”.
Traducido a comandos sobre el VPS de referencia del capítulo 2:
# Cortafuegos: solo SSH y HTTP/HTTPS
sudo ufw default deny incoming && sudo ufw default allow outgoing
sudo ufw allow OpenSSH && sudo ufw allow 'Nginx Full'
sudo ufw enable && sudo ufw status verbose
# MariaDB escuchando solo en loopback
sudo sed -i 's/^bind-address.*/bind-address = 127.0.0.1/' /etc/mysql/mariadb.conf.d/50-server.cnf
sudo systemctl restart mariadb
sudo ss -tlnp | grep 3306 # debe mostrar solo 127.0.0.1:3306
# SSH sin contrasenas ni root
sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo systemctl reload ssh
# Parches automaticos del sistema operativo y fail2ban
sudo apt -y install unattended-upgrades fail2ban
sudo dpkg-reconfigure -plow unattended-upgrades
sudo systemctl enable --now fail2ban && sudo fail2ban-client status
En el lado de la aplicación, mantén security.mode en strict: en ese modo las cookies usan SameSite=Strict y HttpOnly. Y no subas session_lifespan por comodidad: 7200 segundos son dos horas, y cada hora extra es una hora más de ventana para una sesión robada. Con todo montado, así queda el orden de las capas:
graph TD
NET["Red: ufw fail2ban y VPN"] --> EDGE["Borde TLS: HSTS allow por IP y SSO con 2FA"]
EDGE --> WEB["nginx: bloqueo de data vendor install config.php y themes config"]
WEB --> APP["Aplicacion: force_https trusted_proxies rate limiter Antispam y permisos de staff"]
APP --> DATA["Datos: MariaDB en loopback salt protegido y backups fuera del servidor"]
DATA --> OFF["Copia externa verificada con prueba de restauracion"]
Cada capa asume que la anterior puede fallar. Ese es el punto: la restricción por IP no sustituye al rate limiter, y el rate limiter no sustituye a tener MariaDB fuera de internet. La última capa de la aplicación son los permisos de staff, que desde 0.8.4 son por grupo y no por usuario. Revisa que nadie tenga más de lo que necesita; el detalle de cómo se configuran está en el capítulo 9.
Rotación de logs: lo que rota solo y lo que tienes que rotar tú
FOSSBilling rota sus propios logs y lo hace bien. src/library/FOSSBilling/Monolog.php define 11 canales —activity, application, cron, database, license, mail, event, routing, billing, security, email— y cada uno usa un RotatingFileHandler con maxFiles = 90, es decir, retención de 90 días.
$rotatingHandler = new RotatingFileHandler($path, 90, Level::Debug);
Los ficheros quedan en <path_data>/log/<canal>/<canal>-YYYY-MM-DD.log. Lo que no pasa por Monolog y por tanto no rota solo:
| Fichero | Origen |
|---|---|
data/log/php_error.log | ini_set('error_log', ...) en load.php |
data/log/exception_handler.log | Manejador de excepciones de último recurso |
/var/log/fossbilling-cron.log | Redirección de salida del cron en el crontab |
/var/log/cron.log | Dentro del contenedor Docker oficial |
sudo tee /etc/logrotate.d/fossbilling > /dev/null <<'EOF'
/var/lib/fossbilling/data/log/php_error.log
/var/lib/fossbilling/data/log/exception_handler.log
/var/log/fossbilling-cron.log
{
weekly
rotate 12
compress
delaycompress
missingok
notifempty
copytruncate
su www-data www-data
create 0640 www-data www-data
}
EOF
sudo logrotate -d /etc/logrotate.d/fossbilling
copytruncate no es opcional aquí. PHP-FPM mantiene abierto el descriptor de fichero de php_error.log; si logrotate renombra el fichero sin truncar el original, PHP sigue escribiendo en el inode viejo y el fichero nuevo queda vacío para siempre. El -d del último comando es un ensayo en seco: revisa la salida antes de dejarlo activo. Y vigila el tamaño total con du -sh, porque 90 días por 11 canales crece.
Backups: no hay módulo de backup y el salt es lo irrecuperable
Hallazgo negativo verificado: FOSSBilling no tiene módulo de backup en el core. Búsqueda sobre el árbol completo del tag 0.8.5, 1.598 rutas: cero coincidencias de backup. Las únicas menciones a respaldos en la documentación son las advertencias de “haz backup antes de actualizar”. El respaldo es responsabilidad íntegra del operador, y esta es la jerarquía de lo que duele perder:
| Elemento | Criticidad | Por qué |
|---|---|---|
config.php | Máxima | Contiene info.salt |
| Base de datos | Máxima | Clientes, facturas, pedidos, transacciones, tickets |
data/uploads | Alta | Ficheros de productos descargables y adjuntos |
themes/ | Media | Solo si hay temas propios o configuración de tema |
modules/ | Media | Solo si hay módulos de terceros |
data/log | Baja | Auditoría y forense |
data/cache | Nula | Regenerable |
Fíjate en el orden: config.php va por delante del dump. info.salt es la clave de cifrado reversible con la que se guardan las credenciales de pasarelas de pago y de server managers. La documentación es tajante: “Keep this secret and don’t change it after installation.” Sin el salt original, restauras la base de datos y te encuentras con credenciales de Stripe, de cPanel y de tu registrador que no se pueden descifrar. Tendrías que reintroducirlas todas a mano, suponiendo que las tengas en otro sitio. Eso son las integraciones de los capítulos 10, 11 y 12 muertas de golpe. El pipeline completo:
flowchart LR
A["mariadb-dump con single-transaction"] --> C["Artefactos con fecha"]
B["tar de config.php data themes y modules"] --> C
C --> D["Verificacion con gzip -t y tar tzf"]
D --> E["Retencion local de 30 dias"]
E --> F["Copia fuera del servidor con rclone"]
F --> G["Prueba de restauracion en una base aparte"]
G --> H["Recuento de filas de client e invoice"]
#!/usr/bin/env bash
# /usr/local/bin/fossbilling-backup.sh
set -euo pipefail
FB=/var/www/fossbilling
DEST=/var/backups/fossbilling
STAMP=$(date +%F)
mkdir -p "$DEST"
mariadb-dump --defaults-file=/root/.my.cnf \
--single-transaction --routines --triggers --events \
--default-character-set=utf8mb4 \
fossbilling | gzip -9 > "$DEST/db-$STAMP.sql.gz"
tar czf "$DEST/data-$STAMP.tar.gz" -C "$FB" config.php data themes modules
# Un backup que no se abre no es un backup
gzip -t "$DEST/db-$STAMP.sql.gz"
tar tzf "$DEST/data-$STAMP.tar.gz" > /dev/null
find "$DEST" -name '*.gz' -mtime +30 -delete
rclone copy "$DEST" remoto:fossbilling-backups --max-age 25h
--single-transaction da un volcado consistente sin bloquear tablas InnoDB, así que puedes correrlo con el sitio en producción. --routines --triggers --events evita el clásico “restauré y faltan los triggers”.
Las credenciales van en /root/.my.cnf con permisos 600 (sudo chmod 600 /root/.my.cnf), no en la línea de comandos donde cualquiera las ve con ps:
[client]
user=fossbilling
password=LaContrasenaDeLaBD
Programación diaria en el crontab de root:
15 3 * * * /usr/local/bin/fossbilling-backup.sh >> /var/log/fossbilling-backup.log 2>&1
Si operas con Docker, el árbol entero de la aplicación vive en el volumen fossbilling montado en /var/www/html, así que hay que respaldar el volumen además del dump:
docker compose exec -T mariadb mariadb-dump --single-transaction --routines --triggers \
-u fossbilling -p"$MARIADB_PASSWORD" fossbilling | gzip > backup-db-$(date +%F).sql.gz
docker run --rm -v fossbilling:/data:ro -v "$PWD":/backup \
alpine tar czf /backup/backup-app-$(date +%F).tar.gz -C /data .
Prueba de restauración y verificación del respaldo
Un backup no verificado no es un backup, y la verificación tiene dos niveles. Nivel uno: integridad del artefacto. Ya está en el script, con gzip -t y tar tzf. Detecta un dump truncado por disco lleno o una transferencia cortada.
Nivel dos: restauración real. Importa el dump en una base aparte y cuenta filas. Esto detecta lo que la integridad no ve: un dump sintácticamente válido pero vacío porque el usuario no tenía privilegios sobre las tablas.
mariadb -e "CREATE DATABASE fossbilling_restore CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
gunzip -c /var/backups/fossbilling/db-2026-08-09.sql.gz | mariadb fossbilling_restore
mariadb fossbilling_restore -e "
SELECT COUNT(*) AS clientes FROM client;
SELECT COUNT(*) AS facturas FROM invoice;"
El juego de caracteres importa: crea la base en utf8mb4 con utf8mb4_unicode_ci, que es lo que usa el esquema desde que 0.8.0 migró desde utf8. Si la creas con el charset por defecto del servidor, puedes acabar con acentos y emojis corruptos en nombres de clientes.
Compara los recuentos con los de producción. Si difieren, tienes un problema hoy, no el día del incidente. Limpia después con DROP DATABASE fossbilling_restore, hazlo mensualmente y anota la fecha de la última prueba superada: es la única métrica de backup que significa algo.
Rendimiento: OPcache, pool de PHP-FPM, Twig y caché de assets
OPcache
El instalador no se conforma con extension_loaded: llama a opcache_get_status() y comprueba $status['opcache_enabled'], así que instalada pero desactivada cuenta como ausente.
; /etc/php/8.3/fpm/conf.d/10-opcache.ini
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=192
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=1
opcache.revalidate_freq=2
opcache.save_comments=1
Dos directivas merecen explicación porque son contraintuitivas. Uno. opcache.save_comments=1 es obligatorio. FOSSBilling usa Doctrine y atributos de PHP, y hay librerías que leen los comentarios del código. Ponerlo a 0 “por rendimiento” rompe el ORM de formas difíciles de diagnosticar.
Dos. opcache.validate_timestamps=0 rompería el actualizador integrado. Da el máximo rendimiento, pero el actualizador escribe ficheros nuevos en caliente y OPcache seguiría sirviendo los viejos. El propio código lo tiene en cuenta llamando a opcache_invalidate(PATH_CONFIG, true) tras escribir config.php, y el changelog de 0.8.1 registra “refreshed OPcache after config preservation”. Si usas el actualizador integrado, déjalo en 1.
Pool de PHP-FPM
Punto de partida para un VPS de 2 a 4 GB, en /etc/php/8.3/fpm/pool.d/www.conf:
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500
request_terminate_timeout = 300
request_terminate_timeout = 300 da margen al cron disparado por HTTP y a las actualizaciones, que son las dos operaciones largas del sistema. pm.max_requests = 500 recicla los workers y evita que una fuga de memoria acumulada tumbe el pool. Estos valores son un punto de partida basado en práctica general; el proyecto no publica recomendaciones de tuning de FPM.
Twig
Con auto_reload => false eliminas un stat() por plantilla y petición; el precio es limpiar caché con console.php cache:clear cada vez que tocas un tema. Deja strict_variables en true aunque parezca excesivo: muchos bugfixes de 0.8.3 fueron errores que esa opción destapa, y en producción prefieres un error visible a una plantilla que renderiza mal en silencio.
'twig' => [
'debug' => false,
'auto_reload' => false,
'cache' => '/var/lib/fossbilling/data/cache',
'strict_variables' => true,
],
Assets y base de datos
La plantilla oficial de nginx usa expires off para css|img|js|flv|swf|download, lo que desaprovecha por completo la caché del navegador. Los assets de /public llevan hash de build generado por esbuild, así que se pueden cachear agresivamente:
location ^~ /public/ {
expires 30d;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
gzip on;
gzip_vary on;
gzip_comp_level 5;
gzip_min_length 512;
gzip_types text/plain text/css text/javascript application/javascript application/json image/svg+xml;
Un efecto secundario poco obvio del cron: clearOldSessions() purga las sesiones caducadas en cada ejecución con DELETE FROM session WHERE created_at <= :age, donde age = time() - security.session_lifespan. Si el cron no corre, la tabla session crece sin control y nadie relaciona la degradación con el cron parado. Vigila el tamaño de tablas con una consulta sobre information_schema.tables filtrando por table_schema = 'fossbilling' y ordenando por data_length + index_length.
Actualizar sin romper: el flujo de dos fases y el cron pausado
Desde 0.8.1 la actualización integrada tiene dos fases separadas, y entender por qué es la clave para no dejarla a medias:
- Install. “New files are downloaded and deployed. Maintenance mode is enabled and you are logged out.”
- Finalize. “After logging back in, FOSSBilling shows a finalization page where you apply any pending database patches. Once patches are complete, maintenance mode is restored to its previous state and you return to the dashboard.”
La justificación oficial del diseño es literal: “This prevents older versions from automatically applying database patches before the new code is in place.”
stateDiagram-v2
[*] --> Estable
Estable --> Descargando: System Update
Descargando --> Desplegando: ZIP verificado
Desplegando --> Mantenimiento: maintenance_mode activado
Mantenimiento --> SesionCerrada: cache limpiada y sesion cerrada
SesionCerrada --> Finalizacion: el admin vuelve a entrar
Finalizacion --> AplicandoParches: Apply Patches and Update Configuration
AplicandoParches --> Estable: maintenance_mode restaurado
Finalizacion --> CronPausado: si nadie finaliza
CronPausado --> Finalizacion: sigue bloqueado hasta finalizar
Advertencia: el estado de finalización pendiente pausa el cron por completo. No lo ralentiza ni lo degrada: lo detiene. El paso 0 de Cron\Service::runCrons() es este:
if ($this->di['update_finalization']->isRequired()) {
$this->di['logger']->setChannel('cron')->warning('Skipped cron execution because update finalization is pending.');
throw new \FOSSBilling\InformationException('Update finalization is pending. Cron jobs are paused until finalization is completed.', [], 503);
}
Traducido a consecuencias de negocio: no se generan facturas de renovación, no se suspenden los pedidos expirados, no se cancelan los suspendidos, no se envía la cola de correo. Si actualizas un viernes por la tarde y no completas la finalización, el lunes no ha salido ni una factura. Es el fallo operativo más caro de toda la secuencia y el menos evidente, porque el sitio funciona con normalidad.
Antes de actualizar, tres comprobaciones. Uno. Salida a GitHub. FOSSBilling\Update::$allowedDownloadPrefixes solo admite dos orígenes: https://github.com/FOSSBilling/FOSSBilling/releases/ y https://api.github.com/repos/FOSSBilling/FOSSBilling/releases/assets/. Si tu servidor no alcanza esos hosts, el actualizador no funciona. La FAQ confirma además que no funciona en entornos IPv6-only: “Automatic updates depend on GitHub releases for downloads, and GitHub does not fully support IPv6-only access for all required release assets.”
Dos. Permisos. Desde 0.8.2, FOSSBilling\UpdateReadinessCheck verifica antes de tocar nada que el usuario del servidor web puede escribir en config.php, data/, data/cache, data/log, data/uploads, data/update-finalization.json, vendor/, library/, modules/, themes/, public/ y locale/. Y comprueba aparte que install/ sea eliminable, con este comentario en el código: “The install/ folder must be removable. We test it explicitly because is_writable() on a directory does not guarantee the directory can be deleted.”
Tres. Backup. El aviso oficial es de tipo danger: “Always back up first. Before any update, make full backups of your database and all FOSSBilling files.” Si algo se atasca tienes dos salidas: el patcher por CLI, sudo -u www-data php console.php system:run-patcher, y el patcher de emergencia por HTTP en https://billing.example.com/run-patcher. La documentación advierte sobre el segundo: “If you need the fallback patcher, there is often an underlying permissions issue interrupting the normal update flow.” Necesitarlo es en sí mismo un diagnóstico.
Gotcha específico de Docker: el compose oficial monta el árbol entero de la aplicación como volumen en /var/www/html. Cambiar el tag de la imagen de 0.8.5 a 0.8.6 y hacer docker compose up -d no actualiza nada, porque el volumen tapa el contenido de la imagen. La actualización tiene que hacerse desde dentro de FOSSBilling, con el actualizador integrado, que sí escribe en el volumen.
Monitorización y checklist final de puesta en producción
Desde 0.8.5, cron.php devuelve código de salida distinto de cero si alguna tarea aislada falló:
// Surface partial failures (e.g. an isolated cron task that threw) via a non-zero exit code so
// system cron monitors relying on the process exit status can detect them.
if (Environment::isCLI() && !$success) {
exit(1);
}
Las notas de release lo resumen: “Partial cron failures now exit non-zero so monitoring tools can detect them.” Eso habilita un dead man’s switch real: el ping externo solo se dispara si el cron terminó bien.
*/5 * * * * /usr/bin/php8.3 /var/www/fossbilling/cron.php >> /var/log/fossbilling-cron.log 2>&1 && curl -fsS -m 10 https://hc-ping.com/TU-UUID > /dev/null
Si el cron falla o no corre, el ping no llega y el servicio de monitorización te avisa. Es mucho mejor que fiarte del aviso del panel, y aquí hay una discrepancia que conviene conocer. Discrepancia verificada sobre el umbral de retraso del cron. La documentación dice: “If more than 15 minutes pass without a run, FOSSBilling will show a warning in the admin panel.” Pero el código de Cron\Service::isLate() compara contra otra cosa:
public function isLate(): bool
{
$t1 = new \DateTime($this->getLastExecutionTime());
$t2 = new \DateTime('-6min');
return $t1 < $t2;
}
Seis minutos, no quince. Es coherente con una cadencia de cinco minutos más un minuto de margen. No se localizó el punto de la capa de presentación que consume isLate(), así que no se puede descartar un segundo umbral de quince minutos en la vista; lo seguro es que a partir de seis minutos sin ejecución el sistema ya lo considera un retraso. Verifícalo en tu versión leyendo ese método. El valor persistido lo tienes en la tabla setting, en la fila con param = 'last_cron_exec'.
Qué vigilar y con qué:
| Señal | Cómo se comprueba | Umbral |
|---|---|---|
| Cron ejecutándose | Dead man’s switch con hc-ping | 10 minutos sin ping |
| Certificado TLS | certbot renew --dry-run y monitor externo | 20 días para caducar |
| Espacio en disco | df -h y du -sh sobre data/log | 80% de ocupación |
| Backup del día | Presencia y tamaño de db-$(date +%F).sql.gz | Ausente o menor que el 80% del anterior |
| Versión frente a la última publicada | Script de comparación de la sección 2 | Cualquier diferencia |
Canal de log security | tail sobre security-YYYY-MM-DD.log | Revisión semanal |
Tabla session | Consulta de tamaño de tablas | Crecimiento sostenido |
Errores comunes y diagnóstico
| Síntoma | Causa más probable | Diagnóstico | Solución |
|---|---|---|---|
| Bucle de redirección infinito | force_https activo sin trusted_proxies detrás de un proxy | curl -IL y contar saltos | Configurar security.trusted_proxies y reenviar X-Forwarded-Proto: https |
Panel sin estilos, error sha384 en consola | Auto Minify de Cloudflare u otro reescritor de assets rompe SRI | Consola del navegador | Desactivar Auto Minify y cualquier optimización que toque los assets |
| Todos los clientes bloqueados por rate limit | El limitador ve la IP del proxy para todo el tráfico | Revisar security.trusted_proxies | Declarar el proxy como de confianza |
| No se generan facturas ni se envía correo, el sitio funciona | Finalización de actualización pendiente | grep 'Skipped cron execution' data/log/cron/cron-*.log | Entrar al panel y completar System → Update → Apply Patches |
Tabla session enorme y consultas lentas | El cron lleva tiempo sin correr y no purga sesiones | Consulta de tamaño de tablas | Reparar el cron; la purga es parte de runCrons() |
| El cron no corre y no hay error visible | Binario de PHP incorrecto en el crontab | sudo -u www-data /usr/bin/php8.3 .../cron.php; echo $? | Usar ruta absoluta al binario de PHP correcto |
/install sigue accesible tras instalar | El usuario del servidor web no pudo borrar el directorio | test -d install | rm -rfv install y añadir location ^~ /install/ { return 403; } |
config.old.php descargable | Hueco de la plantilla oficial de nginx | curl -o /dev/null -w '%{http_code}' | Añadir location = /config.old.php { return 403; } |
| Credenciales de pasarelas ilegibles tras restaurar | Se restauró la base de datos con otro info.salt | Comparar el salt del config.php restaurado | Recuperar el config.php original; si no existe, reintroducir todas las credenciales |
php_error.log deja de crecer tras rotar | logrotate sin copytruncate, PHP-FPM escribe en el inode viejo | lsof -p $(pgrep -f php-fpm) buscando el fichero borrado | Añadir copytruncate y reiniciar PHP-FPM |
| Actualizador falla al descargar | Sin salida a GitHub, o entorno IPv6-only | curl -I https://api.github.com desde el servidor | Habilitar salida a GitHub o actualizar manualmente |
| Actualizador se niega a empezar | UpdateReadinessCheck detecta rutas no escribibles | La propia pantalla lista las rutas | chown -R www-data:www-data sobre la raíz y chmod -R 775 data |
Panel de admin accesible desde internet pese al allow | Se cambió admin_area_prefix y el location sigue apuntando a /admin | nginx -T y buscar el bloque location del panel | Alinear el location con el nuevo prefijo |
| Errores de permisos tras ejecutar la CLI | Se ejecutó console.php como root y la caché quedó de root | ls -la data/cache | chown -R www-data:www-data data y ejecutar siempre con sudo -u www-data |
Checklist de puesta en producción
Recórrela entera antes de abrir el registro público. Cada línea tiene su comando o su sección en este capítulo.
Versión y parches
-
console.php system:versiondevuelve 0.8.5 o superior, y no ninguna rama 0.7.x - Suscripción activa a releases y security alerts del repositorio
-
update_branchenrelease, no enpreview
Transporte y borde
- Certificado TLS válido y renovación probada con
certbot renew --dry-run - Redirección 301 del puerto 80 y
security.force_https => true -
security.trusted_proxiesconfigurado si hay proxy delante - Cabeceras de seguridad aplicadas y verificadas con
curl -I - Auto Minify y cualquier reescritura de assets desactivados en el CDN
Superficie expuesta
-
/installborrado, devolviendo 403 o 404 -
config.php,config.old.php,/data/,/vendor/y/themes/*/config/devuelven 403 -
/public/sigue siendo legible -
path_datafuera de la raíz web, contwig.cachemovido también -
admin_area_prefixcambiado y ellocationde nginx alineado - Acceso al panel restringido por IP, VPN o SSO con 2FA
Aplicación
-
security.modeenstrictysession_lifespansin aumentar por comodidad -
debug_and_monitoring.debugenfalseyapi.CSRFPreventionentrue - Rate limiter activo con
whitelist_ipsmínima - Antispam con honeypot y un proveedor de CAPTCHA
- Permisos de staff revisados por grupo
Servidor
-
ufwactivo con solo SSH y HTTP/HTTPS abiertos - MariaDB escuchando solo en
127.0.0.1 - SSH sin contraseñas y sin login de root
-
unattended-upgradesyfail2baninstalados y activos -
config.phpen modo640y propiedad dewww-data
Operación
- Cron cada 5 minutos con ruta absoluta al binario de PHP correcto
- Dead man’s switch encadenado con
&&al cron -
logrotateconcopytruncateparaphp_error.log,exception_handler.logy el log del cron - Backup diario verificado con
gzip -tytar tzf, y copiado fuera del servidor - Prueba de restauración superada y fecha anotada
-
config.phprespaldado aparte, porque elsaltes lo irrecuperable - Monitorización de disco, certificado y presencia del backup del día
Cierre del curso y próximos pasos
Este era el último capítulo. Empezaste en el capítulo 1 mirando qué es realmente FOSSBilling y en qué estado está, y terminas con una instalación endurecida, respaldada, monitorizada y con un procedimiento de actualización que no te deja el cron parado sin avisar. Lo que queda por delante no es material de curso, es rutina.
Mensual. Prueba de restauración completa con recuento de filas. Revisión del canal de log security. Comprobación de la versión instalada frente a la última publicada.
Cada release. Leer las notas antes de actualizar, no después. Comprobar si el release trae advisories asociados. Actualizar en una ventana en la que puedas completar la fase de finalización el mismo día.
Cuando el proyecto cierre el issue #62. Ese es el de autenticación de dos factores, asignado al milestone 0.9.x. El día que se fusione, las mitigaciones de infraestructura de este capítulo pasan de ser obligatorias a ser defensa en profundidad. Hasta entonces, siguen siendo lo único que tienes.
Y una última consideración de fondo. El milestone 1.0.x tiene 34 issues abiertos, entre ellos la reescritura completa del sistema de impuestos, la del módulo de carrito y una revisión legal pendiente. Ninguno tiene fecha comprometida. FOSSBilling es hoy un proyecto en clara aceleración —de 10 a 20 commits semanales hace un año a 40 o 70 ahora, con seis releases en menos de dos meses—, pero sigue siendo beta declarada. Opéralo con esa expectativa y te dará un sistema de facturación completo y sin licencias. Opéralo como si fuera un producto cerrado y estable, y el primer sobresalto te encontrará sin backup verificado.