Parcourir la source

Add server-side Graphite Basic authentication over HTTPS

main
psarria il y a 17 heures
Parent
révision
1174c120de
6 fichiers modifiés avec 205 ajouts et 7 suppressions
  1. +13
    -0
      AUDIT.md
  2. +20
    -4
      README.md
  3. +3
    -0
      config.example.php
  4. +110
    -0
      docs/GRAPHITE-APACHE.md
  5. +24
    -2
      lib/Graphite.php
  6. +35
    -1
      tests/run.php

+ 13
- 0
AUDIT.md Voir le fichier

@@ -76,3 +76,16 @@ pendientes. Los avisos de copyright y licencia de componentes ajenos se conserva
No se ha modificado la semántica heredada de éxito/error de suspensión/reactivación.
La API heredada puede anunciar éxito sin comprobar HTTP; la mejora de ese contrato queda
fuera de esta intervención para evitar una reescritura del provisioning.

## Actualización: autenticación HTTP Basic de Graphite

Se añade `graphite_auth` (`none`/`basic`), `graphite_username` y `graphite_password`,
únicamente en configuración privada. cURL envía las credenciales por HTTPS; no se añaden a
URLs ni a la estructura devuelta al frontend. Configuraciones incompletas o inválidas se
rechazan antes de realizar la petición. No se siguen redirecciones ni hay fallback anónimo
al usar Basic. Las respuestas 401/403 conservan el error discreto del área de cliente.

Validación tras esta actualización: 192 aserciones PHP y 24 frontend, lint de todos los PHP,
diff check y auditoría del árbol. Las nuevas credenciales de prueba son sentinelas `FAKE_`.
Se incluye una guía para Apache 2.4. No se han configurado ni verificado el VirtualHost ni
la autenticación del servidor Graphite real; el cambio del cliente no bloquea acceso público.

+ 20
- 4
README.md Voir le fichier

@@ -102,10 +102,26 @@ respuesta limitada a 2 MiB. Un fallo HTTP, timeout, JSON inválido o dato mal fo
Las series ausentes pueden convivir con las disponibles. No se registran cuerpos HTTP,
excepciones detalladas, configuración, credenciales ni parámetros completos de WHMCS.

No se incluyen credenciales de Graphite en la implementación. Esta versión requiere que
el servidor WHMCS pueda acceder al Render API con la configuración de acceso de la red;
si la instalación exige un mecanismo adicional de autenticación, debe integrarse del
lado servidor antes del despliegue. Nunca añadir tokens a URLs públicas.
Graphite admite autenticación HTTP Basic sobre HTTPS mediante configuración privada:

```php
'graphite_auth' => 'basic',
'graphite_username' => '', // Completar solo en el fichero privado.
'graphite_password' => '', // Completar solo en el fichero privado.
```

Usuario y contraseña son de una cuenta técnica exclusiva para WHMCS, no de los clientes.
Se envían mediante las opciones de autenticación cURL, nunca en la URL, HTML o JavaScript.
Una configuración Basic incompleta o inválida impide enviar la petición; no hay fallback
anónimo. HTTP 401/403 y redirecciones muestran el aviso genérico de métricas no disponibles.
TLS sigue siendo obligatorio. `none` conserva compatibilidad con instalaciones anteriores,
pero NO protege Graphite: para el despliegue protegido seleccionar `basic`.

La protección de entrada se configura en Apache; actualizar este módulo no cierra por sí
solo el acceso público. Seguir [la guía Apache](docs/GRAPHITE-APACHE.md), incluyendo la
protección de rutas Render/API y puertos de backend. No compartir la cuenta técnica con
clientes. `graphs_authorization_confirmed` debe permanecer en `false` para la vista Graphs
antigua, que carece de autorización por VPS.

## Interfaz y acceso al panel



+ 3
- 0
config.example.php Voir le fichier

@@ -2,6 +2,9 @@
return [
'vpsmanager_api_url' => '',
'graphite_url' => '',
'graphite_auth' => 'none', // Set to basic when the Graphite HTTPS proxy requires it.
'graphite_username' => '',
'graphite_password' => '',
'graphs_authorization_confirmed' => false,
'graphs_url' => '', // Optional existing full graphs UI base URL; review its authorization.
];

+ 110
- 0
docs/GRAPHITE-APACHE.md Voir le fichier

@@ -0,0 +1,110 @@
# Graphite privado con Apache y WHMCS

Esta guía es una propuesta para Apache 2.4. No se ha aplicado ni validado contra el
VirtualHost de producción: hay que inspeccionar su configuración actual antes de integrarla.
El módulo PHP ya admite las credenciales; el cierre del acceso público se hace en Apache.

## 1. Cuenta técnica

Crear una cuenta HTTP Basic exclusiva para el servidor WHMCS, por ejemplo `whmcs-graphite`.
No es una cuenta de cliente ni un usuario del panel ISPConfig. No compartirla con clientes:
puede acceder a todas las métricas disponibles en Graphite. WHMCS sigue autorizando qué
servicio puede visualizar cada cliente antes de solicitar sus datos.

En una instalación tipo Debian/Ubuntu, usar un fichero de contraseñas fuera del DocumentRoot,
por ejemplo `/etc/apache2/graphite.htpasswd`. Estos comandos solicitan la clave de forma
interactiva y evitan sobrescribir un fichero ya existente:

```bash
if [ -e /etc/apache2/graphite.htpasswd ]; then
sudo htpasswd -B /etc/apache2/graphite.htpasswd whmcs-graphite
else
sudo htpasswd -cB /etc/apache2/graphite.htpasswd whmcs-graphite
fi
```

No usar `htpasswd -b` ni poner la contraseña en argumentos. `-B` genera un hash bcrypt;
`-c` se usa solamente al crear el fichero. Darle permisos `0640`, propietario administrador
y grupo legible por el usuario real de Apache. Adaptar ruta, usuario y grupo al sistema;
no usar `chmod 777` ni guardar este fichero en Git.

## 2. Proteger el VirtualHost HTTPS completo

Integrar este bloque en el VirtualHost HTTPS **existente de Graphite**, conservando su
certificado y sus directivas WSGI/proxy/estáticos actuales:

```apache
<Location "/">
AuthType Basic
AuthName "Graphite privado"
AuthBasicProvider file
AuthUserFile /etc/apache2/graphite.htpasswd
Require user whmcs-graphite
</Location>
```

Se requieren los módulos `auth_basic`, `authn_file` y `authz_user`. Revisar que ninguna
sección más específica (Location, Directory, alias u otro VirtualHost) elimine o sustituya
esta protección. Deben quedar protegidas tanto la interfaz como `/render/`, las APIs de
búsqueda de métricas y todas las demás rutas. Proteger únicamente la página de login no basta.

El VirtualHost HTTP debe redirigir a HTTPS sin servir Graphite. El backend WSGI/proxy debe
escuchar solo en loopback/red privada protegida; cerrar cualquier puerto/origen/hostname
alternativo que permita eludir Apache. Revisar todos los virtual hosts que sirvan el mismo
backend. No activar logs que incluyan Authorization ni modo verbose de cURL.

Antes de recargar, comprobar la configuración:

```bash
sudo apachectl configtest
```

Solo después de obtener `Syntax OK`, recargar Apache con el mecanismo propio del sistema.
No se incluye un VirtualHost completo para evitar sustituir o romper el despliegue existente.

## 3. Configuración privada de WHMCS

En `/etc/open6hosting/whmcs-vpsmanager.php`, conservar las otras claves y añadir:

```php
'graphite_auth' => 'basic',
'graphite_username' => 'whmcs-graphite',
'graphite_password' => '', // Introducir aquí la clave real, solo en este fichero privado.
```

Mantener `graphite_url` como la base HTTPS que ya se utiliza, sin `/render/` y sin
credenciales o query string. Las claves vacías del ejemplo NO son una configuración válida
para Basic: hasta completarlas el módulo muestra el aviso de métricas no disponibles.

La contraseña real debe ser la misma creada en Apache. No copiar el hash bcrypt del fichero
htpasswd: cURL necesita el secreto original y Apache verifica su hash. El fichero privado de
WHMCS debe ser legible por PHP-FPM, no estar dentro del repositorio/DocumentRoot y no ser
escribible por PHP. Restringir igualmente copias de seguridad y acceso administrativo.

## 4. Orden del cambio y verificación

1. Preparar la cuenta y el fichero privado de WHMCS.
2. Instalar esta versión del módulo, manteniendo una copia de seguridad fuera del webroot.
3. Activar la protección de Apache, validar sintaxis y recargar.
4. Desde fuera, sin credenciales, comprobar respuesta **401** en interfaz y Render API.
5. Con contraseña incorrecta, comprobar **401** y que no se devuelve ningún dato.
6. Desde WHMCS, con la cuenta correcta, comprobar **200 JSON** de Render y las tres gráficas.
7. Con dos clientes WHMCS, comprobar que ninguno puede visualizar el servicio del otro.
8. Verificar que el backend y los nombres/puertos alternativos no exponen Graphite.

Para una comprobación manual autenticada, `curl --user whmcs-graphite` solicita la
contraseña sin incluirla en los argumentos. Usar la URL HTTPS propia y no activar `-v`.
No probar con credenciales incluidas en la URL ni copiar respuestas de producción a Git.

La cuenta Basic autentica al servidor WHMCS frente a Graphite. La autorización individual
por cliente sigue siendo responsabilidad de WHMCS; Apache no filtra targets por UUID.
Esta autenticación tampoco protege automáticamente la aplicación independiente Graphs:
si esa aplicación consulta Graphite desde su servidor, puede seguir publicando los datos.
Mantener su botón desactivado y restringir o corregir esa aplicación por separado.

## Referencias

- [Apache 2.4: autenticación y autorización](https://httpd.apache.org/docs/2.4/howto/auth.html)
- [Apache htpasswd](https://httpd.apache.org/docs/2.4/programs/htpasswd.html)
- [cURL HTTPAUTH](https://curl.se/libcurl/c/CURLOPT_HTTPAUTH.html)
- [cURL USERNAME](https://curl.se/libcurl/c/CURLOPT_USERNAME.html)

+ 24
- 2
lib/Graphite.php Voir le fichier

@@ -17,11 +17,26 @@ final class Graphite
];
private $baseUrl;
private $transport;
private $username;
private $password;

public function __construct(array $config, ?callable $transport = null)
{
$this->baseUrl = Config::url($config, 'graphite_url');
$this->transport = $transport ?? [self::class, 'request'];
$mode = $config['graphite_auth'] ?? 'none';
$username = $config['graphite_username'] ?? '';
$password = $config['graphite_password'] ?? '';
if (!in_array($mode, ['none', 'basic'], true)
|| !is_string($username) || !is_string($password)
|| ($mode === 'none' && ($username !== '' || $password !== ''))
|| ($mode === 'basic' && ($username === '' || $password === ''
|| strpos($username, ':') !== false
|| preg_match('/[\\x00-\\x1f\\x7f]/', $username . $password)))) {
throw new \RuntimeException('Configuración de autenticación Graphite no válida.');
}
$this->username = $mode === 'basic' ? $username : null;
$this->password = $mode === 'basic' ? $password : null;
$this->transport = $transport ?? [$this, 'request'];
}

public static function range($value): string
@@ -118,7 +133,7 @@ final class Graphite
];
}

private static function request(string $url): array
private function request(string $url): array
{
if (!function_exists('curl_init')) {
throw new \RuntimeException('Graphite transport unavailable.');
@@ -141,6 +156,13 @@ final class Graphite
return strlen($chunk);
},
]);
if ($this->username !== null) {
curl_setopt_array($ch, [
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERNAME => $this->username,
CURLOPT_PASSWORD => $this->password,
]);
}
try {
$ok = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);


+ 35
- 1
tests/run.php Voir le fichier

@@ -6,7 +6,7 @@ if (extension_loaded('curl')) {
}
foreach (['CURLOPT_RETURNTRANSFER', 'CURLOPT_CONNECTTIMEOUT', 'CURLOPT_TIMEOUT', 'CURLOPT_SSL_VERIFYPEER',
'CURLOPT_SSL_VERIFYHOST', 'CURLOPT_POSTFIELDS', 'CURLOPT_HTTPHEADER', 'CURLOPT_FOLLOWLOCATION',
'CURLOPT_PROTOCOLS', 'CURLPROTO_HTTPS', 'CURLOPT_WRITEFUNCTION', 'CURLINFO_HTTP_CODE'] as $i => $name) {
'CURLOPT_PROTOCOLS', 'CURLPROTO_HTTPS', 'CURLOPT_WRITEFUNCTION', 'CURLINFO_HTTP_CODE', 'CURLOPT_HTTPAUTH', 'CURLAUTH_BASIC', 'CURLOPT_USERNAME', 'CURLOPT_PASSWORD'] as $i => $name) {
define($name, $i + 1);
}
$GLOBALS['calls'] = [];
@@ -220,6 +220,40 @@ try {
check(vpsmanager_ConfigOptions($params)['Nodo']['Options'] === ['fixture-choice'], 'nodes configuration');
$GLOBALS['response'] = ['status' => 500, 'body' => 'PRIVATE_RESPONSE'];
check(vpsmanager_ConfigOptions($params)['Pack']['Options'] === [], 'configuration failure handled');
// Graphite server-to-server Basic authentication. All credentials here are fictitious.
$auth = $config + ['graphite_auth' => 'basic', 'graphite_username' => 'FAKE_GRAPHITE_USER',
'graphite_password' => 'FAKE_GRAPHITE_PASSWORD'];
$GLOBALS['response'] = ['status' => 200, 'body' => json_encode($raw)];
$secured = new Graphite($auth);
$secured->getForService($params);
$call = end($GLOBALS['calls']);
check($call->options[CURLOPT_HTTPAUTH] === CURLAUTH_BASIC, 'explicit Basic authentication');
check($call->options[CURLOPT_USERNAME] === $auth['graphite_username'], 'server-side username');
check($call->options[CURLOPT_PASSWORD] === $auth['graphite_password'], 'server-side password');
check(strpos($call->url, 'FAKE_GRAPHITE') === false, 'credentials absent from URL');
check($call->options[CURLOPT_FOLLOWLOCATION] === false, 'credentials cannot follow redirects');
check($call->options[CURLOPT_SSL_VERIFYPEER] === true, 'authenticated TLS verified');
writeConfig($temp, $auth);
foreach ([200, 401, 403, 302] as $status) {
$GLOBALS['response']['status'] = $status;
$html = vpsmanager_ClientArea($params);
check(strpos($html, 'FAKE_GRAPHITE') === false, 'authentication absent from frontend');
check(strpos($html, base64_encode($auth['graphite_username'] . ':' . $auth['graphite_password'])) === false, 'no Basic header in HTML');
if ($status !== 200) {
check(strpos($html, 'Las métricas no están disponibles temporalmente.') !== false, 'auth/redirect failures are discreet');
}
}
foreach ([['graphite_username' => ''], ['graphite_password' => ''], ['graphite_auth' => 'unknown'],
['graphite_auth' => 'none'], ['graphite_username' => 'user:name'], ['graphite_password' => "FAKE_BAD\r\nvalue"],
['graphite_password' => []], ['graphite_url' => 'http://metrics.example.invalid']] as $override) {
$before = count($GLOBALS['calls']);
rejects(fn() => new Graphite(array_replace($auth, $override)), 'invalid auth configuration rejected');
check(count($GLOBALS['calls']) === $before, 'invalid auth sends no request');
}
writeConfig($temp, $config);
$GLOBALS['response'] = ['status' => 200, 'body' => json_encode($raw)];
(new Graphite($config))->getForService($params);
check(!isset(end($GLOBALS['calls'])->options[CURLOPT_HTTPAUTH]), 'legacy configuration remains supported');
echo "PASS: $count assertions; mocked HTTP only.\n";
} finally {
unlink($temp);


Chargement…
Annuler
Enregistrer