# MySQL

Bases MySQL 8 o MariaDB en el VPS con cuenta propia que exige TLS, permisos solo sobre su base y acceso externo por IP.

Fuente: https://ardumaker.com/docs/mysql/

- Categoría: Servidores e infraestructura
- Estado: Disponible
- Requisitos: Un VPS activo con la función `mysql` disponible; Si el motor ya existe: MySQL 8.x o MariaDB 10.4 o superior; Ser miembro con acceso al VPS para instalar; admin para revelar, abrir acceso o borrar datos
- Herramientas MCP (Solo leen): `ver_catalogo_apps`, `ver_preflight_app`, `ver_app`, `ver_acceso_app`, `ver_operacion_app`, `listar_motores_db_vps`, `listar_bases_db_vps`
- Herramientas MCP (Escriben o modifican): `instalar_app`, `revelar_credencial_app`, `cambiar_lista_ip_app`, `reintentar_operacion_app`
- Herramientas MCP (Destructivas): `revocar_acceso_app`, `desinstalar_app`

## ¿Qué permite hacer?

Cada base MySQL es una app con su cuenta propia. El motor (MySQL o MariaDB) es uno por VPS y lo comparten sus bases; si no existe, se instala MySQL desde los paquetes de la distribución.

- Crear varias bases `utf8mb4`, cada una con su cuenta y sus permisos.
- Revelar usuario, contraseña y URI de conexión.
- Abrir el motor a internet solo para ciertas IP, con TLS.
- Desde la pestaña MySQL del VPS: bases, usuarios, permisos y tablas.

## ¿Cuándo conviene usarlo?

- Aplicaciones que esperan MySQL (PHP, Laravel, muchas herramientas de terceros).
- Un [WordPress](https://ardumaker.com/docs/wordpress) usa este mismo motor, con una base y una cuenta propias por sitio.

## ¿Cómo funciona en ArduMaker?

1. Motor: si el agente dice que no está instalado, se instala una vez (`mysql-server` y `mysql-client`); si ya existe, nunca se reinstala ni cambia de versión.
2. Variante: se lee `mysql --version`. Solo MySQL 8.x o MariaDB 10.4+; otra cosa falla `motor_no_soportado` sin ejecutar SQL.
3. Credencial `app<id>_<hex>` con contraseña aleatoria, guardada cifrada antes de tocar el VPS.
4. Se entra como administrador por socket, con la contraseña en un archivo de opciones efímero, nunca en la línea de comandos.
5. Cuenta `'app<id>_<hex>'@'127.0.0.1'` con `REQUIRE SSL`, y `GRANT ALL PRIVILEGES` solo sobre `<base>.*`, sin `GRANT OPTION`, antes de crear la base.
6. `CREATE DATABASE <base> CHARACTER SET utf8mb4` solo si no existía.
7. Verificación: TLS exigido, sin privilegios globales y permisos sobre una sola base, la suya.

La cuenta es `@'127.0.0.1'` a propósito: no `%` (abriría la cuenta a cualquier origen) ni `localhost` (socket, sin TLS). Cuando el servidor no tiene `partial_revokes` (o es MariaDB), el `_` del nombre se escapa en el GRANT, porque ahí es un comodín.

`config.external_access` decide si la base sale a internet: `{"ips": [...]}` abre solo a esas IP; `{"local": true}` (o `false`) la deja solo adentro del VPS. Si no se indica, desde el panel se toma la IP de quien instala; por MCP la IP del agente no cuenta, así que sin `ips` la base queda local en vez de fallar.

## ¿Qué cambia realmente?

| Qué | Detalle |
| --- | --- |
| Motor | MySQL o MariaDB escuchando en `127.0.0.1:3306` |
| Base | `utf8mb4`; un nombre igual con otras mayúsculas cuenta como el mismo |
| Cuenta | `'app<id>_<hex>'@'127.0.0.1'` REQUIRE SSL |
| TLS en MySQL 8 | `SET PERSIST` + `ALTER INSTANCE RELOAD TLS`, sin reinicio |
| TLS en MariaDB | Archivo `90-ardumaker-tls.cnf`; la primera vez requiere reiniciar el motor |

> **INFO:** No se aceptan como nombre `mysql`, `information_schema`, `performance_schema` ni `sys`. `require_secure_transport` no se activa globalmente: cortaría a los clientes internos sin TLS; el TLS se exige por cuenta.

## Acceso desde internet y lista de IP

Por defecto el motor escucha solo dentro del VPS (3306 en `127.0.0.1`). Para usar la base desde afuera, ArduMaker publica un endpoint propio del motor: `db-<id del VPS>` dentro de la zona de la plataforma (registro DNS directo, sin proxy), en un puerto reservado entre 61000 y 64999. En el host, un proxy lleva ese puerto al 3306 del VPS y el firewall deja entrar solo las IP de la lista.

El certificado es propio del nombre `db-<id>` y lo termina `mysqld`: el proxy del host solo pasa TCP. La cuenta de la app exige SSL venga por donde venga.

| Regla de la lista | Detalle |
| --- | --- |
| Formato | IPv4 o IPv6, con o sin prefijo; una IP suelta es /32 o /128. Nada de nombres, loopback, link-local ni multicast |
| Amplitud | Prefijo mínimo /24 en IPv4 y /48 en IPv6 por defecto; `0.0.0.0/0` y `::/0` se rechazan en la lista |
| Tamaño | Hasta 20 entradas; vacía = cerrado para todos |
| Nombres de host | Hasta 5 (`allowed_hosts`), con hasta 8 IP públicas cada uno; se vuelven a resolver cada 5 minutos y una falla de DNS conserva lo último conocido |
| Abierto a todos | Solo con `allow_any` y `confirm_open` juntos; la respuesta lo dice en `open_to_any` |

La lista es del motor, no de cada base: cambiarla desde cualquier base de MySQL cambia el acceso de todas las bases de ese motor en ese VPS (la respuesta lo indica con `shared_with_engine` y `affected_apps`).

1. Se agregan las reglas de firewall que faltan, una por CIDR (`ufw allow from <cidr> to any port <puerto> proto tcp`).
2. Después se quitan las que sobran y se cortan sus conexiones abiertas.
3. Recién entonces se crea el listener y se verifica: reglas iguales a la lista, ninguna regla abierta a cualquiera, listener presente y handshake TLS con el nombre correcto.

> **INFO:** Agregar antes de quitar hace que el estado intermedio sea siempre la unión de dos listas válidas: nunca hay un instante abierto a todos. Si la verificación falla, se borra el listener.

> **PRECAUCION:** Otro VPS del mismo host no llega por la IP pública aunque esté en la lista: la conexión se corta por tiempo. La respuesta avisa con `warnings` (`mismo_host`); entre dos VPS así se usa un túnel SSH (`configurar_tunel_entre_vps`).

## Desinstalar, persistencia y copias

- Sin `delete_data` (lo normal): se cierra el acceso externo propio y la app deja de figurar como instalada, pero la base, su usuario y sus datos quedan en el VPS. La siguiente reconciliación la muestra como detectada. No se pierde nada.
- Con `delete_data: true` (confirmado dos veces, solo admin): se borra la base y, si la cuenta la creó Apps, también la cuenta y su contraseña guardada. Nunca una cuenta del cliente ni `root`.
- El motor nunca se desinstala desde Apps: queda instalado aunque no tenga bases.
- Si el VPS o el motor no responden, `desinstalar_app` con `forzar=true` intenta cada paso una vez, da de baja la app y lista en `not_cleaned` lo que quedó en el servidor.
- Los datos viven en el disco del VPS y viajan en su copia semanal, que se toma con el VPS andando. Para una copia consistente de una base concreta conviene además un volcado propio desde el [gestor de bases de datos](https://ardumaker.com/docs/gestor-de-bases-de-datos) (exportar) o con las herramientas del motor.

> **PELIGRO:** Con `delete_data` la baja es irreversible.

## Esquema

**Conexiones a una base MySQL**

```text
App en el mismo VPS (`127.0.0.1:3306`)
  ↓
Cliente externo (`db-<id>:<puerto>` con verificación de identidad TLS)
  ↓
Firewall y proxy del host (solo IP de la lista → 3306 del VPS)
  ↓
mysqld (termina TLS; la cuenta exige SSL y ve solo su base)
```

## Resultado esperado

La URI (sin contraseña) aparece cuando el acceso externo está listo; la completa se obtiene con `revelar_credencial_app`.

```text
mysql://app<id>_<hex>@db-<vps>.<zona>:<puerto>/<base>?ssl-mode=VERIFY_IDENTITY
mysql://app<id>_<hex>@db-<vps>.<zona>:<puerto>/<base>?sslMode=verify-full   (MariaDB)
```

## Errores y resolución

| Error | Causa | ¿Reintentar? |
| --- | --- | --- |
| `motor_no_soportado` | No es MySQL 8.x ni MariaDB 10.4+; no se toca nada | No |
| `base_ya_existe` | Hay una base con ese nombre (sin distinguir mayúsculas) sobre la que la cuenta no tiene permisos | No |
| `motor_detenido` / `base_no_creada` / `permisos_no_verificados` | Motor parado, SQL fallido o permisos que no verificaron | Sí |
| `requiere_reinicio` | MariaDB necesita reiniciar para el primer TLS; el acceso no se abre hasta resolverlo | Tras reiniciar |
| Acceso externo no pedido | El motor tiene cuentas anónimas o sin contraseña que el listener expondría | Tras limpiar esas cuentas |

Las demás causas (lista de IP, TLS, credenciales antiguas, cierre no verificado) son las mismas que en [PostgreSQL](https://ardumaker.com/docs/postgresql). El seguimiento es con `ver_operacion_app` y `reintentar_operacion_app`.

## Relación con MCP

Todo el ciclo de la base se opera por MCP con los permisos de la persona que autorizó al agente. Referencia en [/mcp/docs](https://ardumaker.com/mcp/docs) y [/mcp/bases-de-datos](https://ardumaker.com/mcp/bases-de-datos).

- Lectura: `ver_catalogo_apps`, `ver_preflight_app`, `listar_apps_empresa`, `ver_app`, `ver_acceso_app` (host, puerto, TLS y lista de IP, sin secretos), `ver_operacion_app`.
- Modifican: `instalar_app` con `app_type: mysql` (exige `idempotency_key`), `cambiar_lista_ip_app` (solo si su dueño es admin; las IP se nombran explícitas: la IP del agente no es la de la persona), `reintentar_operacion_app`, `refrescar_apps`.
- Sensible: `revelar_credencial_app` (solo admin, no acepta `idempotency_key`; la respuesta viene marcada `sensible: true` y la auditoría registra que se reveló, nunca el valor).
- Destructivas: `revocar_acceso_app` (cierra el acceso externo de todo el motor) y `desinstalar_app` (con `delete_data` borra los datos; requiere `confirm_delete_data`).

Para leer y escribir datos dentro de la base, el agente usa las herramientas del [gestor de bases de datos](https://ardumaker.com/docs/gestor-de-bases-de-datos). Un agente autorizado solo lectura no ve las herramientas que modifican ni puede revelar credenciales.

## Funciones relacionadas

- [Gestor de bases de datos](https://ardumaker.com/docs/gestor-de-bases-de-datos/)
- [PostgreSQL](https://ardumaker.com/docs/postgresql/)
- [WordPress](https://ardumaker.com/docs/wordpress/)
- [Firewall y puertos](https://ardumaker.com/docs/firewall-y-puertos/)
- [Copias de seguridad](https://ardumaker.com/docs/backups/)
