CLI
Tres subcomandos. La config por defecto es condor.yaml en el cwd; se cambia con -c/--config.
$ condor check -c condor.yaml # valida y sale (exit 0 si es válida) $ condor start -c condor.yaml # inicia listeners + endpoints de gestión $ condor reload -c condor.yaml # POST /reload al admin_addr de la config
El reload también se dispara con SIGHUP al proceso (systemctl reload condor).
global
Ajustes de proceso: log y endpoints de gestión, más los límites de robustez (anti-DoS) y el apagado ordenado. Todos los límites traen defaults sanos.
global: log_level: info # debug | info | warn | error log_format: json # json | pretty metrics_addr: "127.0.0.1:9090" # /health, /metrics (bindealo local salvo scrape externo) admin_addr: "127.0.0.1:9091" # + /config, /reload (solo local) watch_config: true # vigila el archivo y recarga en caliente al cambiar # --- límites de conexión / robustez (anti-DoS) --- max_connections: 10000 # tope global de conexiones simultáneas max_connections_per_ip: 100 # tope por IP de ORIGEN (socket TCP, no XFF) tls_handshake_timeout_secs: 10 # completar el handshake TLS header_read_timeout_secs: 30 # enviar headers completos (anti-slowloris) max_request_body_mb: 100 # cuerpo máximo de request → 413 max_header_count: 100 # nº máximo de headers (header flood) request_timeout_secs: 60 # tiempo hasta la respuesta del upstream → 408 graceful_shutdown_enabled: true # drenar conexiones en vuelo al apagar shutdown_timeout_secs: 30 # máximo para drenar antes de forzar
RUST_LOGsobreescribelog_levelsi está presente. Conwatch_config: true(por defecto) Condor vigila tantocondor.yamlcomo los certificados en modofile, y recarga sin reiniciar. Los límites de robustez no se recargan en caliente (requieren reinicio).
max_connections_per_ipcuenta la IP del socket TCP, noX-Forwarded-For. Condor se asume en el borde (ve la IP real). Detrás de un CDN/LB que no preserve la IP, el cupo se contabiliza contra la IP del balanceador — súbelo o desactívalo.request_timeout_secsacota el tiempo hasta la cabecera de la respuesta del upstream, no el streaming del cuerpo: SSE y descargas largas no se cortan.
Compresión (estilo nginx gzip), opt-in bajo global.compression:
compression: enable: true # default false min_length: 1024 # no comprimir respuestas más chicas (bytes) level: default # fast | default | best algorithms: [gzip] # [gzip, br] requiere compilar --features brotli content_types: [text/, application/json, …]
Streaming (no bufferiza el cuerpo). Salta automáticamente respuestas ya comprimidas, 204/304/206, HEAD, text/event-stream (SSE) y Cache-Control: no-transform. Respeta los q-values de Accept-Encoding y añade Vary. gRPC (application/grpc) no se comprime por defecto.
listeners & TLS
Cada listener enlaza una dirección. La presencia de la clave tls activa HTTPS. Sin ella, es HTTP en claro. El listener detecta HTTP/1.1 y HTTP/2 automáticamente.
listeners: - addr: "0.0.0.0:80" # HTTP en claro - addr: "0.0.0.0:443" tls: mode: file # file | acme | self_signed cert: /etc/condor/tls/fullchain.pem key: /etc/condor/tls/privkey.pem
mode: file— cargacert+keyen PEM. Soporta SNI multi-dominio. Hot-reload: conwatch_config: true, al reescribir los archivos Condor recarga el certificado sin reiniciar el listener (rotación sin downtime).mode: self_signed— genera un cert autofirmado en memoria (dev / interno).mode: acme— emite con Let's Encrypt (ver abajo).
ACME / Let's Encrypt
Con mode: acme, Condor obtiene y renueva certificados vía desafío HTTP-01. Requiere un listener en el puerto 80 accesible públicamente (sirve /.well-known/acme-challenge/) y DNS apuntando al host.
tls: mode: acme acme_email: ops@example.com acme_domains: ["api.example.com", "www.example.com"] cache_dir: /var/lib/condor/certs # account.json + cert.pem + key.pem
El certificado se cachea en cache_dir y se renueva en segundo plano cuando le quedan menos de 30 días de validez (según el notAfter real del certificado, parseado del x509). El resolver SNI se actualiza en caliente cuando llega el cert nuevo.
mTLS (autenticación de cliente)
Por listener TLS, client_auth exige que el cliente presente un certificado firmado por una CA de confianza. La verificación es 100% rustls (sin OpenSSL).
tls: mode: file cert: /etc/condor/tls/fullchain.pem key: /etc/condor/tls/privkey.pem client_auth: ca: /etc/condor/tls/clients-ca.pem # CA(s) que firman los certs de cliente optional: false # false = exige cert | true = lo permite
optional: false— el handshake falla si el cliente no presenta un cert válido firmado por la CA. Ninguna petición sin cert llega al backend.optional: true— admite conexiones sin cert; el backend decide leyendoX-Client-Cert-Verify(útil para migraciones graduales).
Tras un handshake correcto, Condor reenvía la identidad del certificado al backend en headers autoritativos. Anti-spoofing: antes elimina cualquier X-Client-Cert-* que el cliente intente falsificar, en todos los listeners (en claro incluido, donde Verify queda en NONE).
X-Client-Cert-Verify—SUCCESSsi presentó cert verificado,NONEsi no.X-Client-Cert-Subject— subject DN del cert (RFC 2253), ej.CN=alice,O=falp.X-Client-Cert-Fingerprint— huella SHA-256 del cert (DER), en hexadecimal.
routes
Las rutas se resuelven por especificidad: host exacto → prefijo de path (el más largo gana) → host wildcard (*.dominio) → catch-all.
routes: - name: api match_host: api.example.com # opcional; admite *.example.com match_path: /v1/* # exacto (/health) o prefijo (/x/*) upstream: app-pool # debe existir en upstreams (o usar split, ver abajo) middleware: [ ... ]
Al reenviar, Condor agrega X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host y X-Request-Id (ULID), elimina los headers hop-by-hop, y propaga o genera un traceparent W3C (correlación de trazas con el backend).
traffic splitting (canary)
Una ruta puede repartir su tráfico entre varios upstreams por peso, en lugar de apuntar a uno fijo. split y upstream son excluyentes.
routes: - name: app match_path: /app/* split: - upstream: app-estable weight: 90 - upstream: app-canario weight: 10 # weight: 0 = preparado, sin tráfico
- El reparto usa smooth weighted round-robin: proporciones exactas (9 de cada 10 al estable) e intercaladas, sin ráfagas.
weight: 0deja la entrada lista sin recibir tráfico: para activar el canary o hacer el cutover basta editar los pesos y hacer hot reload — sin reiniciar ni cortar conexiones.- El upstream que atendió cada petición queda registrado en las métricas (
condor_requests_total{upstream}) y en el audit log: el canary se observa por separado. - La elección ocurre después de la auth y el rate limit de la ruta (una petición rechazada no consume turno del reparto).
middleware
Lista ordenada por ruta. Un middleware que rechaza corta el flujo con el status correspondiente.
middleware: - type: auth mode: jwt # jwt | api_key jwks_url: "https://auth.example.com/.well-known/jwks.json" issuer: "https://auth.example.com" # valida iss (recomendado) audience: "api" # valida aud (recomendado) algorithms: ["RS256", "ES256"] # permitidos; default = set asimétrico # mode: api_key → api_keys: ["k1", "k2"] (header X-Api-Key) # mode: basic → realm + users: {alice: "plain:..", bob: "$2b$.."} (bcrypt) - type: ip_acl # control de acceso por IP (ponlo PRIMERO) allow: ["10.0.0.0/8"] # CIDR v4/v6; default-deny fuera del allow deny: ["10.6.6.6"] # deny gana siempre → 403 - type: ratelimit requests_per_second: 500 burst: 100 # token bucket por IP → 429 + Retry-After - type: audit # registra la petición (ver audit) - type: response_headers # headers de RESPUESTA al cliente set: Strict-Transport-Security: "max-age=31536000" X-Content-Type-Options: "nosniff" remove: [server] # oculta el Server del backend - type: redirect # respuesta 3xx (no toca el upstream) to: "https://nuevo.example.com/" status: 308 # 301 | 302 | 307 | 308 (default 308) - type: rewrite # reescribe el prefijo del path strip_prefix: /api # /api/users → /users add_prefix: /v2 # → /v2/users (tras el strip) - type: waf # firewall de aplicación (ver sección WAF) mode: block # detect (audita) | block (403) rulesets: [sqli, xss, path_traversal, cmd_injection]
auth jwt— valida el Bearer contra el JWKS (refrescado en background, con timeout y límite de tamaño); elsubqueda comouser_iden el audit. El algoritmo se fija por config (no se toma del token → sin confusión de algoritmo); si configurásissuer/audiencese exigen y validan. Falla → 401/403.auth api_key— compara el headerX-Api-Keycontra la lista.auth basic— HTTP Basic (RFC 7617). Credenciales por usuario:plain:<pass>(dev) o hash bcrypt$2…(htpasswd, prod). Comparación en tiempo constante; 401 conWWW-Authenticate.ip_acl—allow/denypor IP de origen con CIDR (IPv4/IPv6).denygana; unallowno vacío hace default-deny. Usa la IP del socket TCP (no XFF); las IPv4-mapeadas se canonicalizan. 403 al denegar.waf— firmas de ataque sobre path/query/headers (ver sección WAF). Rulesets integrados + reglas propias;detect/block.ratelimit— token bucket por IP de origen; excedido → 429.redirect— emite301/302/307/308conLocationsin reenviar al upstream (la auth de la ruta se evalúa antes).rewrite— reescribe el prefijo del path (strip_prefix/add_prefix) antes de reenviar.headers— set/remove sobre los headers de la petición al upstream.response_headers— set/remove sobre los headers de la respuesta al cliente (HSTS, nosniff, ocultarServer…).
WAF
El middleware waf inspecciona la línea de petición (path, query) y las cabeceras contra firmas de ataque, y rechaza (o solo audita) las coincidencias. Nativo, no un plugin dinámico: las reglas se compilan en el binario, sin cargar código de terceros en runtime.
middleware: - type: waf mode: block # detect (solo audita) | block (rechaza 403) rulesets: [sqli, xss, path_traversal, cmd_injection] rules: # reglas propias (opcional), pattern = regex - name: bloquea-shell pattern: "/bin/(ba)?sh" targets: [path, query] # path | query | headers (default: los 3)
- Rulesets integrados, curados para alta precisión (pocas firmas de alta confianza, no recall exhaustivo):
sqli,xss,path_traversal,cmd_injection. - Motor
regexde tiempo lineal, sin backtracking → el WAF no puede volverse vulnerable a ReDoS. Un patrón propio que no compila falla al cargar (fail-closed). - Normalización anti-evasión: path y query se percent-decodifican una vez antes de inspeccionar, así un
../escrito%2e%2e%2fse detecta igual. - Modos:
detectcuenta y audita dejando pasar la petición (para estrenar sin romper tráfico);blockademás responde403. Recomendado: arrancar endetect, revisar falsos positivos en el audit, pasar ablock. - Cada match emite
condor_waf_matched_total{route,rule,action}y una línea de log con la regla y la IP de origen.
Inspecciona path/query/headers, no el cuerpo de la petición (evita bufferizarlo y romper el streaming) — cubre la superficie de URL y cabeceras, por donde llega la mayoría de las inyecciones. Como todo middleware, el orden importa: declaralo temprano (idealmente tras ip_acl).
upstreams
Pool de targets con estrategia de balanceo, health checks y circuit breaker.
upstreams: - name: app-pool strategy: round_robin # round_robin (ponderado) | least_conn | random | ip_hash http2: false # true = HTTP/2 hacia el upstream (h2c, o h2 vía ALPN si tls) tls: false # true = conectar por https:// al upstream tls_server_name: backend.internal # SNI/verificación si el target es IP (requiere tls) tls_insecure: false # true = no verificar el cert (solo backends internos) timeout_ms: 30000 # timeout por petición al upstream retries: 0 # reintentos ante fallo de transporte (idempotentes) targets: - { addr: "10.0.0.11:8080", weight: 1 } - { addr: "10.0.0.12:8080", weight: 2 } health_check: path: /health interval_secs: 5 timeout_ms: 2000 healthy_threshold: 2 unhealthy_threshold: 3 circuit_breaker: failure_threshold: 50 # % de error en la ventana para abrir recovery_secs: 30 # tiempo en Open antes de HalfOpen outlier_detection: # eyección pasiva per-target (ver abajo) consecutive_5xx: 5
tls/tls_server_name/tls_insecure— HTTPS hacia el upstream;tls_server_namefija el SNI y el nombre a verificar cuando el target es una IP.timeout_ms— corta la petición al upstream si excede;retriesreintenta solo en fallos de transporte (conexión rechazada / timeout) y solo en métodos idempotentes, eligiendo otro target del pool.
outlier detection (eyección pasiva)
Complementa al health check activo y al circuit breaker (que opera sobre el upstream completo): observa las respuestas del tráfico real y saca de la rotación al target que acumula errores consecutivos (5xx o fallo de transporte).
outlier_detection: consecutive_5xx: 5 # errores seguidos para eyectar (default 5) base_ejection_secs: 30 # duración base; crece linealmente al reincidir max_ejection_secs: 300 # tope del backoff max_ejection_percent: 50 # % máximo del pool eyectado a la vez
- Pasada la eyección, el target se readmite automáticamente; si reincide, la siguiente eyección dura más (backoff lineal, acotado por
max_ejection_secs). - Dos guardas evitan que la detección empeore un incidente: nunca se eyecta el último target servible (mejor degradado que apagado) y
max_ejection_percentacota cuántos targets pueden estar fuera a la vez. - Estado visible en
GET /health(el target figura como"ejected") y en las métricascondor_outlier_*. - Los errores atribuibles al cliente (p. ej. cuerpo que excede el límite → 413) no cuentan contra el target ni contra el circuit breaker.
WebSocket / Upgrade
Automático, sin configuración. Toda petición HTTP/1.1 con Connection: Upgrade (WebSocket u otro protocolo) se tuneliza: Condor preserva los headers Connection y Upgrade hacia el upstream y, ante un 101 Switching Protocols, establece un túnel bidireccional crudo entre cliente y backend. La auth y el rate limit de la ruta se aplican igual al handshake. El túnel no está sujeto a request_timeout (que solo acota el tiempo hasta el 101). Soportado sobre upstreams HTTP/1.1; no hacia upstreams con http2: true.
audit
Log append-only en JSONL, una línea por petición. Nunca modifica líneas existentes (compatible con sinks WORM).
audit: enabled: true path: /var/log/condor/audit.jsonl rotate: daily # daily | never → audit-YYYY-MM-DD.jsonl fields: [timestamp, request_id, client_ip, method, host, path, status, upstream, duration_ms, user_id, trace_id]
Cada línea, con claves compactas (tid = trace-id W3C, para correlacionar con el tracing del backend):
{"ts":"2026-05-24T03:14:15.926Z","rid":"01J...","cip":"1.2.3.4",
"m":"GET","host":"api.example.com","path":"/v1/patients",
"st":200,"up":"app-pool","dur_ms":12,"uid":"alice","tid":"0af765..."}
Cambiaraudit.fieldsrequiere reiniciar el servicio (no se recarga en caliente). El resto (rutas, upstreams, middleware, certificadosfile) sí.
endpoints de gestión
GET /health— estado y salud de upstreams (JSON). Enmetrics_addryadmin_addr.GET /metrics— formato de texto Prometheus.GET /config— config actual sin secretos. Solo enadmin_addr.POST /reload— dispara hot reload. Solo enadmin_addr.
métricas
condor_requests_total{route,upstream,status,method}
condor_request_duration_seconds{route,upstream}
condor_upstream_health{upstream,target}
condor_circuit_state{upstream} # 0=closed 1=open 2=half
condor_active_connections{listener}
condor_tls_handshakes_total{listener,result}
condor_ratelimit_rejected_total{route}
condor_upstream_pool_size{upstream}
condor_connections_rejected_total{listener,scope} # scope=global|per_ip
condor_http_limit_rejected_total{kind} # kind=body|timeout
condor_active_upgrades{upstream} # túneles WebSocket activos
condor_ipacl_rejected_total{route} # rechazos por IP ACL (403)
condor_redirects_total{route,status} # redirects 3xx emitidos
condor_responses_compressed_total{encoding} # respuestas comprimidas
condor_outlier_ejections_total{upstream,target} # eyecciones por outlier detection
condor_outlier_ejected{upstream,target} # 1 = eyectado ahora mismo
condor_waf_matched_total{route,rule,action} # coincidencias del WAF (detect|block)