-
Notifications
You must be signed in to change notification settings - Fork 12
Description
Descripción
Actualmente los endpoints de apps/core son accesibles sin ningún tipo de autenticación. Se necesita proteger todos los endpoints del módulo core para que únicamente las aplicaciones backoffice e investors puedan consumirlos. Cualquier request que no provenga de una de estas dos aplicaciones debe ser rechazado con 401 Unauthorized.
Contexto
Arquitectura del sistema
El sistema está compuesto por tres aplicaciones:
core: API central que expone endpoints de deploy de contratos Soroban y operaciones contra la base de datos (campaigns, vaults, etc).backoffice: Aplicación interna de administración. Consumecorepara gestionar campañas y desplegar contratos.investors: Aplicación orientada a inversores. Consumecorepara leer y escribir datos del flujo de inversión.
Ningún cliente externo debe tener acceso directo a core. La comunicación es estrictamente service-to-service.
Por qué API Keys y no JWT
JWT es el mecanismo correcto cuando hay usuarios humanos autenticándose. En este caso los consumidores son servicios, no usuarios, por lo que JWT añade complejidad innecesaria (emisión de tokens, expiración, refresh, etc).
Una API Key por servicio es la solución estándar para este escenario: simple, sin infraestructura adicional, y suficientemente segura si las keys se generan y almacenan correctamente.
Generación de las API Keys
Cada key debe generarse con crypto.randomBytes(32) de Node.js, que produce 256 bits de entropía aleatoria. Nunca deben ser strings inventados manualmente.
# Key para backoffice
node -e "console.log('BACKOFFICE_API_KEY=' + require('crypto').randomBytes(32).toString('hex'))"
# Key para investors
node -e "console.log('INVESTORS_API_KEY=' + require('crypto').randomBytes(32).toString('hex'))"Los valores generados se distribuyen así:
core.env: almacena ambas keys para poder validar de qué servicio proviene cada request.backoffice.env: almacena únicamente su propia key.investors.env: almacena únicamente su propia key.
Cada aplicación solo conoce su propia key. Nunca se comparten entre sí.
Flujo de autenticación
Cada request de backoffice o investors hacia core debe incluir el header x-api-key con su key correspondiente. El ApiKeyGuard en core intercepta todos los requests, extrae el header, y verifica si la key existe en el conjunto de keys válidas. Si no existe o el header está ausente, el request es rechazado antes de llegar a cualquier controller.
backoffice ──[x-api-key: BACKOFFICE_KEY]──► core (ApiKeyGuard valida) ──► Controller
investors ──[x-api-key: INVESTORS_KEY]───► core (ApiKeyGuard valida) ──► Controller
tercero ──[x-api-key: key-inválida]────► core (ApiKeyGuard rechaza) ──► 401 Unauthorized
Tareas
En apps/core
- Generar las dos API Keys usando el comando indicado en la sección de contexto y agregarlas al
.envdel servidor. Agregar las variables al.env.examplesin valor:
# .env.example
BACKOFFICE_API_KEY=
INVESTORS_API_KEY=- Crear
apps/core/src/common/guards/api-key.guard.tsque implementeCanActivate. El guard debe:- Leer el header
x-api-keydel request entrante. - Verificar si el valor existe en un
Setconstruido desdeprocess.env.BACKOFFICE_API_KEYyprocess.env.INVESTORS_API_KEY. - Retornar
truesi la key es válida. - Lanzar
UnauthorizedExceptionsi la key no existe o el header está ausente.
- Leer el header
// apps/core/src/common/guards/api-key.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
@Injectable()
export class ApiKeyGuard implements CanActivate {
private readonly validKeys = new Set([
process.env.BACKOFFICE_API_KEY,
process.env.INVESTORS_API_KEY,
]);
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const apiKey = request.headers['x-api-key'];
if (!apiKey || !this.validKeys.has(apiKey)) {
throw new UnauthorizedException('Invalid or missing API key');
}
return true;
}
}- Registrar el guard de forma global en
apps/core/src/main.tspara que aplique a todos los endpoints sin necesidad de decorarlo en cada controller:
// apps/core/src/main.ts
import { ApiKeyGuard } from './common/guards/api-key.guard';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new ApiKeyGuard());
await app.listen(3000);
}En apps/backoffice y apps/investors
- Agregar la variable
CORE_API_KEYal.envde cada aplicación con la key que le corresponde. - Agregar
CORE_API_KEY=sin valor al.env.examplede cada una. - Asegurarse de que todos los servicios que realizan HTTP requests hacia
coreincluyan el header en cada llamada:
headers: {
'x-api-key': process.env.CORE_API_KEY,
}Criterios de aceptación
- Un request a cualquier endpoint de
coresin el headerx-api-keyretorna401 Unauthorized. - Un request con una key inválida o no reconocida retorna
401 Unauthorized. - Un request con la key correcta de
backofficeoinvestorspasa el guard y llega al controller. - Las keys nunca están hardcodeadas en el código fuente.
- Las keys no tienen valor en
.env.example, solo el nombre de la variable. - El guard es global; no se añade ningún decorador en controllers ni endpoints individuales.
- No se modifica ningún módulo existente más allá de
main.ts.
Notas adicionales
- En caso de que en el futuro se necesite un endpoint público (health check, por ejemplo), se puede implementar un decorador
@Public()usandoSetMetadatade NestJS que el guard respete, sin necesidad de remover el guard global. - La rotación de una key comprometida consiste en generar un nuevo valor con
crypto.randomBytes(32), reemplazarlo en el.envdel servidor y en la app cliente correspondiente. No requiere downtime ni migraciones. - Las keys deben almacenarse en el gestor de secretos del entorno de producción (AWS Secrets Manager, Doppler, o equivalente). El
.envsolo aplica para desarrollo local.
Metadata
Metadata
Assignees
Type
Projects
Status