1. Introducción al proyecto
El sistema de chat AI Qwen3-VL-8B es una solución web completa que integra una interfaz frontal, un servidor proxy inverso y un motor de inferencia vLLM. Diseñado con una arquitectura modular, permite despliegue local y acceso remoto, ofreciendo servicios de diálogo con IA seguros y confiables para entornos empresariales.
En un contexto corporativo, solo ejecutar el servicio no basta. La seguridad de datos, el cifrado de comunicaciones y la auditoría de operaciones son requisitos críticos. Este artículo detalla cómo configurar certificados TLS, implementar redirección forzada a HTTPS y habilitar un registro de auditoría completo para el sistema Qwen3-VL-8B.
Componentes principales del sistema:
- Interfaz frontal: Chat moderno basado en HTML/CSS/JS
- Servidor proxy: Proxy inverso escrito en Python que maneja archivos estáticos y reenvía API
- Motor de inferencia vLLM: Backend de alto rendimiento que ejecuta el modelo Qwen3-VL-8B
2. Preparación del entorno y configuración base
2.1 Requisitos del sistema
- Ubuntu 20.04+ o CentOS 8+
- Python 3.8+
- Nginx 1.18+ (para proxy HTTPS)
- OpenSSL
- Al menos 8 GB de VRAM
- Nombre de dominio (para solicitud de certificado)
2.2 Instalación de dependencias
sudo apt update && sudo apt upgrade -y
sudo apt install nginx openssl certbot python3-certbot-nginx -y
python3 --version
pip3 --version
pip3 install requests flask cryptography
2.3 Estructura de directorios recomendada
/opt/qwen-chat/
├── app/ # directorio principal de la aplicación
│ ├── chat.html # interfaz de chat
│ ├── proxy_server.py
│ └── static/ # recursos estáticos
├── ssl/ # certificados SSL
│ ├── cert.pem
│ ├── privkey.pem
│ └── chain.pem
├── logs/ # registros
│ ├── access.log
│ ├── error.log
│ └── audit.log
└── config/
├── nginx.conf
└── supervisor.conf
3. Configuración de certificados TLS
3.1 Métodos de obtención de certificados
Certificado gratuito Let's Encrypt (para entornos de prueba):
sudo certbot --nginx -d tudominio.com
Certificado comercial (para producción): Adquirido de una CA de confianza.
Certificado autofirmado (para pruebas internas):
openssl req -x509 -newkey rsa:4096 -keyout privkey.pem -out cert.pem \
-days 365 -nodes -subj "/CN=tudominio.com"
3.2 Instalación y configuración del certificado en Nginx
# /etc/nginx/sites-available/qwen-chat
server {
listen 80;
server_name tudominio.com;
location ~ /.well-known/acme-challenge {
allow all;
root /var/www/html;
}
location / {
return 301 https://$server_name$request_uri;
}
}
server {
listen 443 ssl http2;
server_name tudominio.com;
ssl_certificate /etc/letsencrypt/live/tudominio.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/tudominio.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
location / {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /v1/ {
proxy_pass http://localhost:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
3.3 Renovación automática del certificado
sudo crontab -e
# Añadir tarea: renovar cada domingo a las 2:00 AM
0 2 * * 0 /usr/bin/certbot renew --quiet --post-hook "systemctl reload nginx"
4. Redirección forzada a HTTPS
4.1 Redirección desde Nginx
server {
listen 80;
server_name tudominio.com www.tudominio.com;
location /health {
proxy_pass http://localhost:8000/health;
access_log off;
}
location / {
return 301 https://$server_name$request_uri;
}
}
4.2 Redirección desde la aplicación (segunda capa)
# en proxy_server.py
from flask import request, redirect, Flask
app = Flask(__name__)
@app.before_request
def forzar_https():
if not request.is_secure and request.host != 'localhost:8000':
if request.headers.get('X-Forwarded-Proto') != 'https':
return redirect(request.url.replace('http://', 'https://'), code=301)
4.3 Políticas de seguridad en el frontend
<!-- en chat.html -->
<meta http-equiv="Content-Security-Policy"
content="upgrade-insecure-requests; default-src 'self' https:">
<meta http-equiv="Strict-Transport-Security"
content="max-age=31536000; includeSubDomains">
5. Sistema de registro de auditoría
5.1 Requisitos de auditoría
- Registro de accesos de usuario
- Registro de llamadas a la API
- Eventos de seguridad (accesos anómalos, fallos de autenticación)
- Operaciones del sistema (inicio/parada de servicios, cambios de configuración)
5.2 Implementación del registro de auditoría en el proxy
import logging
import json
from datetime import datetime
logger_auditoria = logging.getLogger('auditoria')
logger_auditoria.setLevel(logging.INFO)
handler_archivo = logging.FileHandler('/opt/qwen-chat/logs/audit.log')
handler_archivo.setFormatter(logging.Formatter('%(asctime)s - %(message)s'))
logger_auditoria.addHandler(handler_archivo)
def registrar_evento(tipo_evento, detalles):
datos = {
'momento': datetime.utcnow().isoformat(),
'tipo': tipo_evento,
'ip_cliente': request.remote_addr,
'agente': request.headers.get('User-Agent'),
'detalles': detalles
}
logger_auditoria.info(json.dumps(datos))
@app.before_request
def log_solicitud():
if request.path != '/health':
registrar_evento('inicio_peticion', {
'metodo': request.method,
'ruta': request.path,
'parametros': dict(request.args)
})
@app.after_request
def log_respuesta(response):
if request.path != '/health':
registrar_evento('fin_peticion', {
'ruta': request.path,
'codigo': response.status_code,
'tamano': response.calculate_content_length()
})
return response
5.3 Registro detallado de llamadas a la API
@app.route('/v1/chat/completions', methods=['POST'])
def chat_completar():
try:
datos = request.get_json()
registrar_evento('llamada_api', {
'endpoint': '/v1/chat/completions',
'modelo': datos.get('model'),
'num_mensajes': len(datos.get('messages', [])),
'max_tokens': datos.get('max_tokens')
})
# Reenviar a vLLM
respuesta = requests.post(
'http://localhost:3001/v1/chat/completions',
json=datos,
timeout=30
)
registrar_evento('respuesta_api', {
'endpoint': '/v1/chat/completions',
'codigo': respuesta.status_code,
'tiempo': respuesta.elapsed.total_seconds()
})
return respuesta.json(), respuesta.status_code
except Exception as e:
registrar_evento('error_api', {
'endpoint': '/v1/chat/completions',
'error': str(e)
})
return jsonify({'error': str(e)}), 500
5.4 Registro mejorado en Nginx
log_format detallado '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'$request_time $upstream_response_time '
'$ssl_protocol $ssl_cipher '
'"$http_x_forwarded_for"';
access_log /var/log/nginx/qwen_access.log detallado;
6. Refuerzo de seguridad y monitorización
6.1 Configuración del firewall
sudo ufw reset
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw default deny incoming
sudo ufw enable
6.2 Script de monitorización
#!/bin/bash
# /opt/qwen-chat/scripts/monitor.sh
verificar_servicio() {
nombre=$1
puerto=$2
if nc -z localhost $puerto; then
echo "$(date): $nombre en puerto $puerto OK"
else
echo "$(date): $nombre en puerto $puerto FALLA"
# Enviar alerta (email, Slack, etc.)
fi
}
verificar_servicio "Nginx" 80
verificar_servicio "Nginx HTTPS" 443
verificar_servicio "Proxy" 8000
verificar_servicio "vLLM" 3001
fecha_caducidad=$(openssl x509 -enddate -noout -in /etc/letsencrypt/live/tudominio.com/cert.pem | cut -d= -f2)
echo "Certificado caduca: $fecha_caducidad"
6.3 Rotación de logs
# /etc/logrotate.d/qwen-chat
/opt/qwen-chat/logs/*.log {
daily
missingok
rotate 30
compress
delaycompress
notifempty
copytruncate
}
7. Despliegue completo
7.1 Script de despliegue automatizado
#!/bin/bash
# deploy_qwen_enterprise.sh
set -e
echo "Iniciando despliegue empresarial de Qwen3-VL-8B..."
mkdir -p /opt/qwen-chat/{app,ssl,logs,config,scripts}
cp chat.html proxy_server.py /opt/qwen-chat/app/
cp start_all.sh /opt/qwen-chat/
apt update && apt install -y nginx openssl certbot python3-certbot-nginx
certbot certonly --standalone -d tudominio.com --non-interactive --agree-tos
cp nginx.conf /etc/nginx/sites-available/qwen-chat
ln -sf /etc/nginx/sites-available/qwen-chat /etc/nginx/sites-enabled/
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
systemctl restart nginx
cd /opt/qwen-chat && ./start_all.sh
cp monitor.sh /opt/qwen-chat/scripts/
cp logrotate-config /etc/logrotate.d/qwen-chat
(crontab -l 2>/dev/null; echo "0 2 * * 0 /usr/bin/certbot renew --quiet --post-hook 'systemctl reload nginx'") | crontab -
(crontab -l 2>/dev/null; echo "*/5 * * * * /opt/qwen-chat/scripts/monitor.sh >> /opt/qwen-chat/logs/monitor.log") | crontab -
echo "Despliegue completado. Acceder a: https://tudominio.com"
7.2 Verificación del despliegue
netstat -tlnp | grep -E '(80|443|8000|3001)'
curl -I https://tudominio.com
curl -I https://tudominio.com/chat.html
curl -X POST https://tudominio.com/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"Qwen3-VL-8B","messages":[{"role":"user","content":"Hola"}]}'
openssl s_client -connect tudominio.com:443 -servername tudominio.com < /dev/null 2>/dev/null | openssl x509 -noout -dates
tail -f /opt/qwen-chat/logs/audit.log
tail -f /var/log/nginx/access.log
7.3 Solución de problemas comunes
- Fallo en la obtención del certificado: verificar resolución DNS, puerto 80 libre, revisar /var/log/letsencrypt/
- HTTPS no funciona: ejecuatr
nginx -t, verificar rutas de certificados, revisar /var/log/nginx/error.log - Los logs de auditoría no se generan: comproabr permisos del directorio logs, permisos de escritura de Python, revisar salida de la aplicación