Skip to content

Seguridad e Integración

La seguridad es un requisito de primera clase en GM Hades Backend. El servicio procesa consultas SQL arbitrarias sobre bases de datos de producción multi-tenant, por lo que cada capa de la arquitectura aplica controles activos: desde la validación en el handler gRPC hasta la construcción de la cadena de conexión en el servicio de dominio.

Validación de input

RFC como identificador de tenant

Todo request que incluye un RFC pasa por utils.IsValidRFC antes de cualquier operación de base de datos. Esta función aplica una expresión regular contra el formato SAT (personas físicas y morales, con y sin homoclave). Un RFC inválido retorna codes.InvalidArgument de inmediato, sin consultar ADMON.

Restricción a SELECT

utils.IsSelectQuery normaliza la consulta recibida (elimina comentarios SQL, trim de whitespace) y verifica que la primera instrucción sea SELECT, SHOW, DESCRIBE o EXPLAIN. Cualquier otro statement retorna codes.InvalidArgument con el mensaje "solo se permiten consultas de lectura". Esta validación opera en el handler, antes de que la consulta llegue al repositorio.

Bounds checking

Los parámetros page_size, page_number, limit y offset tienen valores máximos configurables. Valores fuera de rango se rechazan con codes.InvalidArgument.

Consultas parametrizadas

GM Hades Backend no usa concatenación de strings para construir predicados SQL.

SQL Server (ADMON): las búsquedas dinámicas usan sql.Named de la librería database/sql. Los valores de búsqueda con LIKE se sanitizan con una función interna que escapa los caracteres %, _ y [.

query := `SELECT ... FROM tabla WHERE campo LIKE @search`
rows, err := db.QueryContext(ctx, query, sql.Named("search", "%"+sanitizeLike(term)+"%"))

MariaDB (Bóveda Fiscal): las consultas recibidas del cliente se aceptan solo si pasan IsSelectQuery, y los parámetros se pasan mediante ? placeholders del driver go-sql-driver/mysql.

Sin secretos en logs

Zap registra todos los eventos como campos estructurados (zap.Field). Las contraseñas de ADMON, MariaDB, SSH y SFTP nunca son campos de log. La cadena de conexión completa tampoco se loguea; solo se registra server y database en modo debug. ValidateNoHardcodedSecrets() verifica en startup que ninguna variable de entorno con sufijo _PASSWORD o _KEY tenga su valor codificado directamente en el binario.

Sanitización de errores hacia el cliente

El modelo de error diferencia entre lo que es seguro exponer y lo que debe quedar solo en los logs:

Código gRPCCondiciónMensaje al cliente
codes.InvalidArgumentRFC inválido, query no-SELECT, parámetro fuera de rangoDescripción técnica del problema
codes.NotFoundTenant no encontrado en ADMON"tenant not found"
codes.DeadlineExceededTimeout de contexto superado"query timeout"
codes.InternalError de base de datos, error de red, pánico"internal error" (detalle solo en logs)

Los errores de backend se loguean con el stack trace completo usando zap.Error. El cliente solo recibe el código gRPC y un mensaje genérico para errores internos.

Credenciales desde el entorno

config.LoadConfig usa os.Getenv para todas las variables marcadas como requeridas. Si una variable está vacía y APP_ENV es production, el proceso llama os.Exit(1) antes de crear cualquier listener. No existen valores hardcodeados ni archivos de configuración de credenciales en el repositorio.

Tipos de error de dominio

El paquete internal/errors define tipos de error estructurados que implementan la interfaz error:

TipoCamposDescripción
TimeoutErrorMessage, TimeoutConsulta que excede el timeout configurado
ValidationErrorField, MessageInput inválido en un campo específico
DatabaseErrorOperation, Message, ErrError de base de datos con contexto de operación
InvalidRFCErrorRFCRFC que no pasa la validación de formato SAT
ConnectionErrorServer, Port, Message, ErrFallo al establecer conexión de red
BovedaErrorOperation, RFC, Message, ErrError en operaciones de Bóveda Fiscal; implementa Unwrap() para unwrapping de errores anidados

Todos los tipos implementan Error() string con un mensaje descriptivo que incluye el contexto del error. Los handlers del servicio gRPC convierten estos tipos al código gRPC correspondiente usando un mapper central en internal/errors/grpc.go.

Observabilidad

Logs estructurados con Zap

Cada operación de query registra un evento con campos consistentes:

CampoTipoDescripción
rfcstringRFC del tenant (enmascarado en logs de producción)
operation_tagstringIdentificador del tipo de operación
execution_time_msint64Tiempo de ejecución en milisegundos
row_countintNúmero de filas retornadas
serverstringHost del servidor de base de datos
databasestringNombre de la base de datos

Niveles de log

NivelCuándo se usa
DEBUGDetalles de construcción de query, selección de credenciales de caché
INFOInicio/cierre del servidor, conexiones establecidas, operaciones completadas
WARNReintentos, caché miss, parámetros de configuración subóptimos
ERRORErrores de base de datos, errores de red, validaciones fallidas
FATALError en ValidateSecurityRequirements, imposible continuar

Health check

El servicio implementa el protocolo estándar de gRPC health check:

Terminal window
grpcurl -plaintext localhost:50051 grpc.health.v1.Health/Check

Adicionalmente, el listener HTTP en el mismo puerto expone el endpoint /health para load balancers HTTP como Azure Container Apps:

Terminal window
curl http://localhost:50051/health

Las métricas están disponibles en http://localhost:9090/metrics (Prometheus format) cuando ENABLE_METRICS=true.

GetServiceInfo

QueryGatewayService.GetServiceInfo retorna la versión del binario, commit hash, build time, versión de Go y lista de servicios gRPC registrados. Es útil para verificar que el binario desplegado corresponde al tag esperado.

Testing

Los tests unitarios cubren las capas de mayor riesgo:

ArchivoQué cubre
internal/boveda_fiscal/config/loader_test.goParseo de connections.json, resolución de RFC
internal/boveda_fiscal/db/mariadb_test.goIsSelectQuery con variantes de input
internal/services/services_validation_test.goValidación de RFC, bounds checking, parámetros

Ejecutar tests con detector de data races:

Terminal window
go test ./... -race -count=1 -timeout=120s

Generar reporte de cobertura:

Terminal window
go test ./... -coverprofile=coverage.out && go tool cover -html=coverage.out

Análisis estático:

Terminal window
go vet ./...
staticcheck ./...
gosec -quiet ./...
golangci-lint run

Integración con grpcurl

Listar servicios

Terminal window
grpcurl -plaintext localhost:50051 list

Describir servicio

Terminal window
grpcurl -plaintext localhost:50051 describe QueryGatewayService

Ejecutar una consulta SQL

Terminal window
grpcurl -plaintext -d '{
"query": "SELECT TOP 10 Id, RFC FROM CatUsuarios",
"tenant": { "rfc": "GMX123456789" }
}' localhost:50051 QueryGatewayService/ExecuteQuery

Streaming de resultados

Terminal window
grpcurl -plaintext -d '{
"query": "SELECT * FROM OrdenDeServicio",
"tenant": { "rfc": "GMX123456789" },
"page_size": 1000
}' localhost:50051 QueryGatewayService/StreamQuery

Health check gRPC

Terminal window
grpcurl -plaintext -d '{"service": "QueryGatewayService"}' \
localhost:50051 grpc.health.v1.Health/Check

Listar directorio SFTP

Terminal window
grpcurl -plaintext -d '{
"rfc": "GMX123456789",
"path": "/cfdis/2024"
}' localhost:50051 BovedaFiscalSFTPService/ListDirectory

Descargar lote de CFDIs como ZIP

Terminal window
grpcurl -plaintext -d '{
"rfc": "GMX123456789",
"uuids": ["uuid-1", "uuid-2", "uuid-3"]
}' localhost:50051 BovedaFiscalSFTPService/DownloadBatchAsZip > cfdi_batch.zip

Integración con cliente Go

import (
"context"
"log"
pb "github.com/gmtransport/gm-hades-backend/proto/gen/go/query/v2"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.NewClient(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
client := pb.NewQueryGatewayServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
stream, err := client.StreamQuery(ctx, &pb.StreamQueryRequest{
Query: "SELECT * FROM OrdenDeServicio",
Tenant: &pb.TenantContext{Rfc: "GMX123456789"},
PageSize: 1000,
})
if err != nil {
log.Fatal(err)
}
for {
chunk, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
switch chunk.Type {
case pb.StreamChunkType_METADATA:
fmt.Printf("Columnas: %v\n", chunk.Columns)
case pb.StreamChunkType_DATA:
// chunk.Rows contiene los datos en formato JSON
fmt.Printf("Filas en este chunk: %d\n", len(chunk.Rows))
case pb.StreamChunkType_END:
fmt.Printf("Total filas: %d\n", chunk.TotalRows)
}
}
}

Resumen

  • GM Hades Backend aplica controles de seguridad en cada capa: validación de RFC e IsSelectQuery en el handler, consultas parametrizadas en el repositorio, sin secretos en logs, errores sanitizados hacia el cliente.
  • Los tipos de error de dominio en internal/errors permiten un mapeo claro entre errores de infraestructura y códigos gRPC.
  • Zap registra campos estructurados consistentes en todas las operaciones. Los errores internos tienen stack trace en logs pero el cliente recibe solo codes.Internal.
  • Los tests unitarios cubren las capas de mayor riesgo (validación, parseo de configuración, IsSelectQuery). La cobertura se ejecuta con -race para detectar condiciones de carrera.
  • La integración puede verificarse sin cliente personalizado usando grpcurl para todos los servicios y métodos.