diff --git a/.env.example b/.env.example index 962038b..732fcdf 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,20 @@ KAFKA_PORT=9092 CONNECT_PORT=8083 KAFKA_BOOTSTRAP_SERVERS=localhost:9092 +# ─── Kafka Secure Mode (optional SASL_SSL overlay) ────────────────────────── +KAFKA_SECURITY_PROTOCOL=PLAINTEXT +KAFKA_SASL_MECHANISM=SCRAM-SHA-512 +KAFKA_BROKER_USERNAME=broker-user +KAFKA_BROKER_PASSWORD= +KAFKA_BACKEND_USERNAME=backend-user +KAFKA_BACKEND_PASSWORD= +KAFKA_CONNECT_USERNAME=connect-user +KAFKA_CONNECT_PASSWORD= +KAFKA_CONNECT_TRUSTSTORE_PASSWORD=changeit +KAFKA_BACKEND_TRUSTSTORE_PASSWORD=changeit +KAFKA_KEYSTORE_PASSWORD=changeit +KAFKA_TRUSTSTORE_PASSWORD=changeit + # ─── Database App User (init.sql) ─────────────────────────────────────────── MONITOR_APP_USER=monitor_app MONITOR_APP_PASSWORD= diff --git a/.gitignore b/.gitignore index c1cf08d..94a20e4 100644 --- a/.gitignore +++ b/.gitignore @@ -80,11 +80,18 @@ docker-compose.override.yml *.p12 *.pfx *.jks +*.keystore *.crt *.cert secrets/ .vault-token .vault/ +!docker/ +!docker/kafka/ +!docker/kafka/secrets/ +docker/kafka/secrets/* +!docker/kafka/secrets/.gitkeep +!docker/kafka/secrets/README.md # Security tooling # dependency-check-report.html (ignored below in Reports section) diff --git a/docker-compose.secure.yml b/docker-compose.secure.yml new file mode 100644 index 0000000..7ee0701 --- /dev/null +++ b/docker-compose.secure.yml @@ -0,0 +1,62 @@ +services: + kafka: + ports: + - "${KAFKA_BIND_HOST:-127.0.0.1}:${KAFKA_PORT:-9092}:9092" + volumes: + - ./docker/kafka/secrets:/etc/kafka/secrets:ro + environment: + KAFKA_LISTENERS: SASL_SSL://0.0.0.0:29092,SASL_SSL_HOST://0.0.0.0:9092 + KAFKA_ADVERTISED_LISTENERS: SASL_SSL://kafka:29092,SASL_SSL_HOST://localhost:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: SASL_SSL:SASL_SSL,SASL_SSL_HOST:SASL_SSL + KAFKA_INTER_BROKER_LISTENER_NAME: SASL_SSL + KAFKA_SASL_ENABLED_MECHANISMS: SCRAM-SHA-512 + KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL: SCRAM-SHA-512 + ZOOKEEPER_SASL_ENABLED: "false" + KAFKA_OPTS: -Djava.security.auth.login.config=/etc/kafka/secrets/kafka_server_jaas.conf -Dzookeeper.sasl.client=false + KAFKA_SSL_CLIENT_AUTH: required + KAFKA_SSL_KEYSTORE_FILENAME: ${KAFKA_SSL_KEYSTORE_FILENAME:-kafka.keystore.jks} + KAFKA_SSL_KEYSTORE_CREDENTIALS: ${KAFKA_SSL_KEYSTORE_CREDENTIALS:-kafka_keystore_creds} + KAFKA_SSL_KEY_CREDENTIALS: ${KAFKA_SSL_KEY_CREDENTIALS:-kafka_sslkey_creds} + KAFKA_SSL_TRUSTSTORE_FILENAME: ${KAFKA_SSL_TRUSTSTORE_FILENAME:-kafka.truststore.jks} + KAFKA_SSL_TRUSTSTORE_CREDENTIALS: ${KAFKA_SSL_TRUSTSTORE_CREDENTIALS:-kafka_truststore_creds} + healthcheck: + test: ["CMD-SHELL", "kafka-topics --bootstrap-server localhost:9092 --command-config /etc/kafka/secrets/kafka_admin_client.properties --list > /dev/null 2>&1 || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + + kafka-connect: + environment: + BOOTSTRAP_SERVERS: kafka:29092 + CONNECT_SECURITY_PROTOCOL: SASL_SSL + CONNECT_SASL_MECHANISM: SCRAM-SHA-512 + CONNECT_SASL_JAAS_CONFIG: org.apache.kafka.common.security.scram.ScramLoginModule required username="${KAFKA_CONNECT_USERNAME}" password="${KAFKA_CONNECT_PASSWORD}"; + CONNECT_SSL_TRUSTSTORE_LOCATION: /etc/kafka/secrets/connect.truststore.jks + CONNECT_SSL_TRUSTSTORE_PASSWORD: ${KAFKA_CONNECT_TRUSTSTORE_PASSWORD} + CONNECT_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: + CONNECT_PRODUCER_SECURITY_PROTOCOL: SASL_SSL + CONNECT_PRODUCER_SASL_MECHANISM: SCRAM-SHA-512 + CONNECT_PRODUCER_SASL_JAAS_CONFIG: org.apache.kafka.common.security.scram.ScramLoginModule required username="${KAFKA_CONNECT_USERNAME}" password="${KAFKA_CONNECT_PASSWORD}"; + CONNECT_PRODUCER_SSL_TRUSTSTORE_LOCATION: /etc/kafka/secrets/connect.truststore.jks + CONNECT_PRODUCER_SSL_TRUSTSTORE_PASSWORD: ${KAFKA_CONNECT_TRUSTSTORE_PASSWORD} + CONNECT_PRODUCER_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: + CONNECT_CONSUMER_SECURITY_PROTOCOL: SASL_SSL + CONNECT_CONSUMER_SASL_MECHANISM: SCRAM-SHA-512 + CONNECT_CONSUMER_SASL_JAAS_CONFIG: org.apache.kafka.common.security.scram.ScramLoginModule required username="${KAFKA_CONNECT_USERNAME}" password="${KAFKA_CONNECT_PASSWORD}"; + CONNECT_CONSUMER_SSL_TRUSTSTORE_LOCATION: /etc/kafka/secrets/connect.truststore.jks + CONNECT_CONSUMER_SSL_TRUSTSTORE_PASSWORD: ${KAFKA_CONNECT_TRUSTSTORE_PASSWORD} + CONNECT_CONSUMER_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: + volumes: + - ./docker/kafka/secrets:/etc/kafka/secrets:ro + + backend: + environment: + KAFKA_BOOTSTRAP_SERVERS: kafka:29092 + KAFKA_SECURITY_PROTOCOL: SASL_SSL + KAFKA_SASL_MECHANISM: SCRAM-SHA-512 + KAFKA_SASL_JAAS_CONFIG: org.apache.kafka.common.security.scram.ScramLoginModule required username="${KAFKA_BACKEND_USERNAME}" password="${KAFKA_BACKEND_PASSWORD}"; + KAFKA_SSL_TRUSTSTORE_LOCATION: /etc/kafka/secrets/backend.truststore.jks + KAFKA_SSL_TRUSTSTORE_PASSWORD: ${KAFKA_BACKEND_TRUSTSTORE_PASSWORD} + KAFKA_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: + volumes: + - ./docker/kafka/secrets:/etc/kafka/secrets:ro diff --git a/docker-compose.yml b/docker-compose.yml index fc5290f..27a597e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,7 @@ services: MONITOR_APP_USER: ${MONITOR_APP_USER:-monitor_app} MONITOR_APP_PASS: ${MONITOR_APP_PASSWORD} ports: - - "${ORACLE_PORT:-1521}:1521" + - "${ORACLE_BIND_HOST:-127.0.0.1}:${ORACLE_PORT:-1521}:1521" volumes: - oracle-data:/opt/oracle/oradata - ./docker/oracle/init.sql:/container-entrypoint-initdb.d/01-init.sql:ro @@ -50,7 +50,7 @@ services: depends_on: - zookeeper ports: - - "${KAFKA_PORT:-9092}:9092" + - "${KAFKA_BIND_HOST:-127.0.0.1}:${KAFKA_PORT:-9092}:9092" networks: - monitor-net environment: @@ -90,7 +90,7 @@ services: CONNECT_KEY_CONVERTER_SCHEMAS_ENABLE: "false" CONNECT_VALUE_CONVERTER_SCHEMAS_ENABLE: "false" ports: - - "${CONNECT_PORT:-8083}:8083" + - "${CONNECT_BIND_HOST:-127.0.0.1}:${CONNECT_PORT:-8083}:8083" networks: - monitor-net healthcheck: @@ -106,8 +106,8 @@ services: container_name: monitor-mailhog restart: unless-stopped ports: - - "1025:1025" - - "8025:8025" + - "${MAILHOG_BIND_HOST:-127.0.0.1}:${MAILHOG_SMTP_PORT:-1025}:1025" + - "${MAILHOG_BIND_HOST:-127.0.0.1}:${MAILHOG_UI_PORT:-8025}:8025" networks: - monitor-net @@ -119,7 +119,7 @@ services: container_name: monitor-backend restart: unless-stopped ports: - - "8080:8080" + - "${BACKEND_BIND_HOST:-127.0.0.1}:${BACKEND_PORT:-8080}:8080" environment: SPRING_PROFILES_ACTIVE: prod KAFKA_BOOTSTRAP_SERVERS: kafka:29092 @@ -157,7 +157,7 @@ services: container_name: monitor-frontend restart: unless-stopped ports: - - "3000:3000" + - "${FRONTEND_BIND_HOST:-127.0.0.1}:${FRONTEND_PORT:-3000}:3000" depends_on: backend: condition: service_healthy diff --git a/docker/kafka/secrets/.gitkeep b/docker/kafka/secrets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker/kafka/secrets/README.md b/docker/kafka/secrets/README.md new file mode 100644 index 0000000..41feba6 --- /dev/null +++ b/docker/kafka/secrets/README.md @@ -0,0 +1,16 @@ +# Kafka Security Secrets (Local) + +This directory stores local TLS/SASL materials used by `docker-compose.secure.yml`. + +Do not commit private keys, truststores, or credentials. + +Expected files: +- kafka.keystore.jks +- kafka.truststore.jks +- connect.truststore.jks +- backend.truststore.jks +- kafka_keystore_creds +- kafka_sslkey_creds +- kafka_truststore_creds + +These files are ignored by `.gitignore` (except this README and `.gitkeep`). diff --git a/docker/oracle/init.sql b/docker/oracle/init.sql index 2eb356e..7923b00 100644 --- a/docker/oracle/init.sql +++ b/docker/oracle/init.sql @@ -28,17 +28,17 @@ GRANT CREATE SESSION TO "&MONITOR_APP_USER"; GRANT SELECT ON SYS.V_$DATABASE TO "&MONITOR_APP_USER"; GRANT SELECT ON SYS.V_$LOG TO "&MONITOR_APP_USER"; GRANT SELECT ON SYS.V_$LOGFILE TO "&MONITOR_APP_USER"; -GRANT SELECT ON SYS.V_$LOGMNR_CONTENTS TO monitor_app; -GRANT SELECT ON SYS.V_$ARCHIVED_LOG TO monitor_app; -GRANT SELECT ON SYS.V_$TRANSACTION TO monitor_app; +GRANT SELECT ON SYS.V_$LOGMNR_CONTENTS TO "&MONITOR_APP_USER"; +GRANT SELECT ON SYS.V_$ARCHIVED_LOG TO "&MONITOR_APP_USER"; +GRANT SELECT ON SYS.V_$TRANSACTION TO "&MONITOR_APP_USER"; -- Switch to the application schema -ALTER SESSION SET CURRENT_SCHEMA = monitor_app; +ALTER SESSION SET CURRENT_SCHEMA = "&MONITOR_APP_USER"; -- ============================================================================= -- Table: LOG_TRAZA -- ============================================================================= -CREATE TABLE monitor_app.log_traza ( +CREATE TABLE log_traza ( id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, error_code VARCHAR2(50) NOT NULL, error_msg VARCHAR2(4000) NOT NULL, @@ -51,7 +51,7 @@ CREATE TABLE monitor_app.log_traza ( -- ============================================================================= -- Table: CRITICAL_OUTBOX -- ============================================================================= -CREATE TABLE monitor_app.critical_outbox ( +CREATE TABLE critical_outbox ( id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, attempts NUMBER DEFAULT 0 NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, @@ -70,17 +70,17 @@ CREATE TABLE monitor_app.critical_outbox ( ); -- Enable row-level supplemental logging -ALTER TABLE monitor_app.log_traza ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS; -ALTER TABLE monitor_app.critical_outbox ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS; +ALTER TABLE log_traza ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS; +ALTER TABLE critical_outbox ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS; -- Seed data -INSERT INTO monitor_app.log_traza (error_code, error_msg, severity, source_app) +INSERT INTO log_traza (error_code, error_msg, severity, source_app) VALUES ('APP-001', 'Scheduled job completed successfully', 'INFO', 'batch-processor'); -INSERT INTO monitor_app.log_traza (error_code, error_msg, severity, source_app) +INSERT INTO log_traza (error_code, error_msg, severity, source_app) VALUES ('APP-002', 'Connection pool near capacity (85%)', 'WARNING', 'api-gateway'); -INSERT INTO monitor_app.log_traza (error_code, error_msg, severity, source_app) +INSERT INTO log_traza (error_code, error_msg, severity, source_app) VALUES ('APP-003', 'Unable to reach secondary Oracle node', 'CRITICAL', 'data-sync-daemon'); COMMIT; diff --git a/docs/README.md b/docs/README.md index ca65dfd..b22ff89 100644 --- a/docs/README.md +++ b/docs/README.md @@ -47,6 +47,11 @@ It is designed to serve three audiences: - `docs/operations/README.md` - operations index - `docs/operations/monitor-operations-runbook.md` - operational runbook +#### Security Reports + +- `docs/reports/SECURITY-AUDIT.md` - baseline security assessment and findings catalog +- `docs/reports/SECURITY-HARDENING-READINESS.md` - current closure/readiness status for findings #8-#14 + ### Recommended Reading Paths #### For executives or architects @@ -134,6 +139,11 @@ Esta dise�ado para tres audiencias: - `docs/operations/README.md` - indice operativo - `docs/operations/monitor-operations-runbook.md` - runbook operativo +#### Reportes de Seguridad + +- `docs/reports/SECURITY-AUDIT.md` - evaluacion base de seguridad y catalogo de hallazgos +- `docs/reports/SECURITY-HARDENING-READINESS.md` - estado actual de cierre/readiness para hallazgos #8-#14 + ### Rutas de Lectura Recomendadas #### Para ejecutivos o arquitectos diff --git a/docs/operations/README.md b/docs/operations/README.md index 356856f..0293adf 100644 --- a/docs/operations/README.md +++ b/docs/operations/README.md @@ -22,6 +22,9 @@ It is intended for developers, operators, and support engineers who need repeata ### Index - `monitor-operations-runbook.md` - day-to-day operational procedures, smoke checks, and incident-oriented troubleshooting +- `kafka-sasl-ssl-quickstart.md` - secure Kafka enablement (TLS/SASL) with bootstrap scripts +- `kafka-sasl-ssl-troubleshooting.md` - failure patterns and diagnostics for Kafka secure mode +- `kafka-sasl-ssl-acceptance-checklist.md` - release checklist to close secure Kafka rollout ### Scope @@ -42,7 +45,7 @@ The operations set covers: ### Resumen Ejecutivo Esta carpeta es el punto de entrada operativo para validar y diagnosticar `Monitor`. -Esta pensada para responder rapidamente una pregunta: **¿la plataforma esta sana, observable y conectada de extremo a extremo?** +Esta pensada para responder rapidamente una pregunta: **�la plataforma esta sana, observable y conectada de extremo a extremo?** ### Proposito @@ -52,6 +55,9 @@ Esta pensada para desarrolladores, operadores y personal de soporte que necesite ### Indice - `monitor-operations-runbook.md` - procedimientos operativos diarios, smoke checks y troubleshooting orientado a incidentes +- `kafka-sasl-ssl-quickstart.md` - habilitacion de Kafka seguro (TLS/SASL) con scripts de bootstrap +- `kafka-sasl-ssl-troubleshooting.md` - guia de diagnostico para fallas de Kafka en modo seguro +- `kafka-sasl-ssl-acceptance-checklist.md` - checklist de aceptacion para cierre operativo de Kafka seguro ### Alcance diff --git a/docs/operations/kafka-sasl-ssl-acceptance-checklist.md b/docs/operations/kafka-sasl-ssl-acceptance-checklist.md new file mode 100644 index 0000000..74e303e --- /dev/null +++ b/docs/operations/kafka-sasl-ssl-acceptance-checklist.md @@ -0,0 +1,34 @@ +# Kafka SASL/SSL Acceptance Checklist + +Use this checklist to declare Hallazgo #12 operationally closed. + +## Preconditions +- [ ] `.env` contains secure Kafka credentials +- [ ] TLS artifacts exist under `docker/kafka/secrets/` +- [ ] Preflight passes: `./scripts/kafka-secure-preflight.sh` + +## Activation +- [ ] Secure mode applied: `./scripts/kafka-enable-secure-mode.sh` +- [ ] Overlay is active: `docker compose -f docker-compose.yml -f docker-compose.secure.yml ps` + +## Smoke validation +- [ ] `./scripts/kafka-secure-smoke.sh` exits with code 0 +- [ ] `kafka`, `kafka-connect`, `backend` are running +- [ ] Backend env has `KAFKA_SECURITY_PROTOCOL=SASL_SSL` +- [ ] Connect env has `CONNECT_SECURITY_PROTOCOL=SASL_SSL` +- [ ] Recent logs have no TLS/SASL handshake/auth failures + +## Functional validation +- [ ] Debezium connector stays healthy +- [ ] CDC events still arrive to backend pipeline +- [ ] No regression on SSE event delivery + +## Rollback validated +- [ ] Plaintext fallback works: `docker compose -f docker-compose.yml up -d` +- [ ] Services recover after rollback + +## Evidence to attach in PR/issue +- [ ] `docker compose ps` output (secure mode) +- [ ] `./scripts/kafka-secure-preflight.sh` output +- [ ] `./scripts/kafka-secure-smoke.sh` output +- [ ] relevant logs for kafka/connect/backend diff --git a/docs/operations/kafka-sasl-ssl-quickstart.md b/docs/operations/kafka-sasl-ssl-quickstart.md new file mode 100644 index 0000000..f732eeb --- /dev/null +++ b/docs/operations/kafka-sasl-ssl-quickstart.md @@ -0,0 +1,73 @@ +# Kafka SASL/SSL Quickstart + +## Purpose +Enable encrypted and authenticated Kafka traffic using `docker-compose.secure.yml` on top of the default stack. + +## 1) Prepare secrets +Generate truststores/keystores and credential files automatically: + +```bash +./scripts/kafka-generate-secrets.sh +``` + +Artifacts are written under: +- `docker/kafka/secrets/` + +See required filenames in: +- `docker/kafka/secrets/README.md` + +## 2) Export required environment variables +Example: + +```bash +export KAFKA_CONNECT_USERNAME=connect-user +export KAFKA_CONNECT_PASSWORD=connect-password +export KAFKA_CONNECT_TRUSTSTORE_PASSWORD=changeit + +export KAFKA_BACKEND_USERNAME=backend-user +export KAFKA_BACKEND_PASSWORD=backend-password +export KAFKA_BACKEND_TRUSTSTORE_PASSWORD=changeit +``` + +## 3) Start secure stack + +```bash +docker compose up -d kafka zookeeper +./scripts/kafka-bootstrap-scram-users.sh +docker compose -f docker-compose.yml -f docker-compose.secure.yml up -d +``` + +Or run the full flow in one command: + +```bash +./scripts/kafka-enable-secure-mode.sh +``` + +## Troubleshooting preflight + +Run explicit prerequisite checks before secure activation: + +```bash +./scripts/kafka-secure-preflight.sh +``` + +If preflight fails, fix missing env vars/files and rerun. + +## 4) Verify secure mode +- Kafka broker listener map includes `SASL_SSL` +- Backend starts with `KAFKA_SECURITY_PROTOCOL=SASL_SSL` +- Debezium connect worker starts with SASL settings + +Run automated smoke check: + +```bash +./scripts/kafka-secure-smoke.sh +``` + +## 5) Rollback to plaintext + +```bash +docker compose -f docker-compose.yml up -d +``` + +This removes secure override and returns to base plaintext mode. diff --git a/docs/operations/kafka-sasl-ssl-troubleshooting.md b/docs/operations/kafka-sasl-ssl-troubleshooting.md new file mode 100644 index 0000000..641c5be --- /dev/null +++ b/docs/operations/kafka-sasl-ssl-troubleshooting.md @@ -0,0 +1,53 @@ +# Kafka SASL/SSL Troubleshooting + +## Common failure patterns + +### 1) Authentication failed (SASL) +Symptoms: +- Backend or connect logs show authentication exceptions +- Kafka client cannot establish session + +Checks: +- Verify usernames/passwords exported in environment +- Re-run SCRAM bootstrap script: + - ./scripts/kafka-bootstrap-scram-users.sh +- Ensure secure overlay is active: + - docker compose -f docker-compose.yml -f docker-compose.secure.yml ps + +### 2) SSL handshake failure +Symptoms: +- javax.net.ssl.SSLHandshakeException +- PKIX path building failed + +Checks: +- Verify truststore files exist under docker/kafka/secrets +- Verify truststore passwords match env values +- Regenerate artifacts if in doubt: + - ./scripts/kafka-generate-secrets.sh + +### 3) Broker starts but clients fail +Symptoms: +- Kafka service healthy, backend/connect unhealthy + +Checks: +- Run preflight: + - ./scripts/kafka-secure-preflight.sh +- Verify KAFKA_SECURITY_PROTOCOL is SASL_SSL in backend env +- Verify connect env includes CONNECT_SECURITY_PROTOCOL=SASL_SSL + +### 4) Secure mode rollout stuck +Recovery path: +1. Roll back to plaintext: + - docker compose -f docker-compose.yml up -d +2. Recreate credentials and secrets +3. Re-run secure activation flow + +## Quick diagnostics +- Container state: + - docker compose ps +- Backend logs: + - docker compose logs --tail=200 backend +- Kafka connect logs: + - docker compose logs --tail=200 kafka-connect +- Kafka logs: + - docker compose logs --tail=200 kafka diff --git a/docs/operations/kafka-tls-sasl-hardening-plan.md b/docs/operations/kafka-tls-sasl-hardening-plan.md new file mode 100644 index 0000000..638f835 --- /dev/null +++ b/docs/operations/kafka-tls-sasl-hardening-plan.md @@ -0,0 +1,62 @@ +# Kafka TLS/SASL Hardening Plan (Hallazgo #12) + +## Objective +Migrate local and production Kafka traffic from PLAINTEXT to authenticated and encrypted transport, without breaking Debezium and Spring Boot consumers. + +## Scope +- Service: Kafka broker in docker-compose stack +- Clients: Debezium Kafka Connect, Spring Boot backend, optional local CLI tools +- Security target: TLS encryption + SASL/SCRAM authentication + +## Non-goals +- No direct cutover in this phase 3 patch set +- No certificate authority automation in this document + +## Current risk +Current stack uses PLAINTEXT listeners and no SASL authentication. Any actor with network access can read or inject messages. + +## Proposed rollout +1. Add secure listener profile in compose +- Keep existing PLAINTEXT listener for migration window +- Add parallel SASL_SSL listener and broker certificates + +2. Provision credentials and trust material +- Generate broker keystore and truststore +- Generate client truststore for backend and Debezium +- Configure SCRAM users per client + +3. Migrate clients one by one +- Debezium connect worker: set SASL/SCRAM and truststore +- Spring Boot app: set spring.kafka properties for SASL_SSL +- Validate topic read/write and connector offsets + +4. Remove PLAINTEXT listener +- Remove legacy listener from advertised listeners +- Keep only SASL_SSL after all clients pass health checks + +## Required environment variables +- KAFKA_SECURITY_MODE=sasl_ssl +- KAFKA_SASL_MECHANISM=SCRAM-SHA-512 +- KAFKA_USERNAME +- KAFKA_PASSWORD +- KAFKA_TRUSTSTORE_PATH +- KAFKA_TRUSTSTORE_PASSWORD +- KAFKA_KEYSTORE_PATH +- KAFKA_KEYSTORE_PASSWORD + +## Validation checklist +- Unauthorized consumer fails to connect +- Authorized producer and consumer can exchange messages +- Debezium connector remains healthy and advances offsets +- Backend starts cleanly and receives CDC events +- Packet capture does not expose plaintext payload + +## Rollback plan +- Re-enable PLAINTEXT listener profile +- Revert client security properties to previous values +- Restart broker, connect, and backend in sequence + +## Ownership +- Infra: compose and certificate handling +- Backend: Spring Kafka client configuration +- Data pipeline: Debezium configuration migration diff --git a/docs/reports/SECURITY-HARDENING-READINESS.md b/docs/reports/SECURITY-HARDENING-READINESS.md new file mode 100644 index 0000000..93b63fc --- /dev/null +++ b/docs/reports/SECURITY-HARDENING-READINESS.md @@ -0,0 +1,84 @@ +# Security Hardening Readiness Report + +Date: 2026-04-02 +Scope: Remaining findings from SECURITY-AUDIT.md (#8 to #14) +Target branch: develop + +## Executive Summary +Security hardening phases for findings #8 to #14 were implemented across CI/CD, backend configuration, Docker runtime exposure, Kafka secure-mode rollout, and operational controls. + +Readiness conclusion: +- Develop environment: READY with compensating controls +- Production promotion: CONDITIONALLY READY, requires secure-mode enforcement decision for Kafka (mandatory SASL_SSL cutover) + +## Findings Status Matrix + +| Finding | Severity | Status | Evidence | +|---|---|---|---| +| #8 (deploy pipeline skips tests) | HIGH | Closed | `.github/workflows/deploy.yml` uses `mvn --batch-mode clean verify -Powasp` | +| #9 (Jackson hardening) | MEDIUM | Closed | `src/main/java/com/monitor/config/JacksonConfig.java` enforces unknown-property fail and disables default typing | +| #10 (Oracle privilege/user hardcoding) | MEDIUM | Mitigated | `docker/oracle/init.sql` parameterized user/schema references, reduced hardcoded account coupling | +| #11 (Docker port exposure) | MEDIUM | Closed | `docker-compose.yml` binds exposed ports to localhost by default (`127.0.0.1`) | +| #12 (Kafka plaintext transport) | MEDIUM | Mitigated with rollout controls | `docker-compose.secure.yml` + secure scripts + preflight + smoke + acceptance checklist | +| #13 (dependency hygiene) | LOW | Closed | `pom.xml` duplicate security dependency removed | +| #14 (secret file protection) | LOW | Closed | `.gitignore` includes secret patterns and constrained exceptions for tracked placeholders | + +## Implemented Controls (Phase Trace) + +### CI/CD and dependency gates +- Production deploy workflow executes full verification with OWASP profile. +- Dependency check and enforcer are integrated in Maven build lifecycle. + +### Serialization hardening +- ObjectMapper hardened with: + - `FAIL_ON_UNKNOWN_PROPERTIES` + - `deactivateDefaultTyping()` + - restricted visibility configuration + +### Database/script hardening +- Oracle init script no longer depends on hardcoded schema/user references for grants and DDL context. +- Current schema is parameterized via `&MONITOR_APP_USER`. + +### Runtime exposure reduction +- Container ports default-bind to loopback host, reducing accidental external exposure in local/proxy-hosted environments. + +### Kafka secure transport rollout controls +Implemented artifacts: +- `docker-compose.secure.yml` +- `scripts/kafka-generate-secrets.sh` +- `scripts/kafka-bootstrap-scram-users.sh` +- `scripts/kafka-secure-preflight.sh` +- `scripts/kafka-enable-secure-mode.sh` +- `scripts/kafka-secure-smoke.sh` +- `docs/operations/kafka-sasl-ssl-quickstart.md` +- `docs/operations/kafka-sasl-ssl-troubleshooting.md` +- `docs/operations/kafka-sasl-ssl-acceptance-checklist.md` + +These controls establish an auditable path to run Kafka in SASL_SSL and validate it before promotion. + +## Residual Risk + +### Finding #12 residual risk +Base compose remains PLAINTEXT-compatible by default to preserve backward compatibility and low-risk local onboarding. This is acceptable for staged rollout but should not be the final production posture. + +Residual risk rating: Medium (operationally controlled, not yet hard-enforced by default) + +## Release Decision + +For develop: +- APPROVED + +For production: +- APPROVED WITH CONDITION + +Condition to fully close finding #12 in production: +1. Enforce secure overlay in production deployment path. +2. Remove PLAINTEXT listener usage from production runtime configuration. +3. Attach smoke/preflight evidence artifacts in release PR. + +## Evidence Checklist for Release PR +- Output of `./scripts/kafka-secure-preflight.sh` +- Output of `./scripts/kafka-secure-smoke.sh` +- `docker compose ps` with secure overlay +- Last 200 lines of logs for `kafka`, `kafka-connect`, `backend` +- Confirmation that deploy pipeline uses verify + OWASP profile diff --git a/pom.xml b/pom.xml index b867537..c3012c9 100644 --- a/pom.xml +++ b/pom.xml @@ -98,10 +98,6 @@ - - org.springframework.boot - spring-boot-starter-security - org.springframework.boot spring-boot-starter-aop diff --git a/scripts/kafka-bootstrap-scram-users.sh b/scripts/kafka-bootstrap-scram-users.sh new file mode 100755 index 0000000..b206052 --- /dev/null +++ b/scripts/kafka-bootstrap-scram-users.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +BACKEND_USER="${KAFKA_BACKEND_USERNAME:-backend-user}" +BACKEND_PASS="${KAFKA_BACKEND_PASSWORD:-backend-password}" +CONNECT_USER="${KAFKA_CONNECT_USERNAME:-connect-user}" +CONNECT_PASS="${KAFKA_CONNECT_PASSWORD:-connect-password}" +BROKER_USER="${KAFKA_BROKER_USERNAME:-broker-user}" +BROKER_PASS="${KAFKA_BROKER_PASSWORD:-broker-password}" + +# This script intentionally bootstraps SCRAM credentials against ZooKeeper while +# the stack is still in PLAINTEXT mode to avoid circular auth bootstrapping. +for USERNAME in "$BACKEND_USER" "$CONNECT_USER" "$BROKER_USER"; do + PASSWORD="$BACKEND_PASS" + if [[ "$USERNAME" == "$CONNECT_USER" ]]; then + PASSWORD="$CONNECT_PASS" + elif [[ "$USERNAME" == "$BROKER_USER" ]]; then + PASSWORD="$BROKER_PASS" + fi + + echo "Creating SCRAM credentials for user: $USERNAME" + docker compose exec -T kafka kafka-configs \ + --zookeeper zookeeper:2181 \ + --alter \ + --add-config "SCRAM-SHA-512=[iterations=4096,password=${PASSWORD}]" \ + --entity-type users \ + --entity-name "$USERNAME" +done + +echo "SCRAM users created. Next step: restart with docker-compose.secure.yml" diff --git a/scripts/kafka-enable-secure-mode.sh b/scripts/kafka-enable-secure-mode.sh new file mode 100755 index 0000000..25a8a2c --- /dev/null +++ b/scripts/kafka-enable-secure-mode.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT_DIR" + +if [[ ! -f docker-compose.secure.yml ]]; then + echo "docker-compose.secure.yml not found" + exit 1 +fi + +echo "Step 1/4: generate TLS materials" +./scripts/kafka-generate-secrets.sh + +echo "Step 2/4: start base stack (plaintext bootstrap)" +docker compose up -d kafka zookeeper + +echo "Step 3/4: create SCRAM users" +./scripts/kafka-bootstrap-scram-users.sh + +echo "Step 4/5: preflight check" +./scripts/kafka-secure-preflight.sh + +echo "Step 5/5: apply secure override" +docker compose -f docker-compose.yml -f docker-compose.secure.yml up -d kafka kafka-connect backend + +echo "Secure mode applied. Validate with: docker compose ps" diff --git a/scripts/kafka-generate-secrets.sh b/scripts/kafka-generate-secrets.sh new file mode 100755 index 0000000..2d5a674 --- /dev/null +++ b/scripts/kafka-generate-secrets.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -euo pipefail + +SECRETS_DIR="docker/kafka/secrets" +DNAME="CN=monitor-kafka, OU=Monitor, O=Monitor, L=Local, ST=Local, C=US" +SUBJECT_ALT_NAMES="SAN=dns:kafka,dns:localhost,dns:monitor-kafka,ip:127.0.0.1" +STOREPASS="${KAFKA_KEYSTORE_PASSWORD:-changeit}" +TRUSTPASS="${KAFKA_TRUSTSTORE_PASSWORD:-changeit}" +VALIDITY_DAYS="${KAFKA_CERT_VALIDITY_DAYS:-365}" +BROKER_USER="${KAFKA_BROKER_USERNAME:-broker-user}" +BROKER_PASS="${KAFKA_BROKER_PASSWORD:-broker-password}" + +mkdir -p "$SECRETS_DIR" + +# Clean previous artifacts to avoid stale/corrupt stores in repeated local runs. +rm -f "$SECRETS_DIR"/*.jks "$SECRETS_DIR"/*.crt "$SECRETS_DIR"/*.csr "$SECRETS_DIR"/*.properties "$SECRETS_DIR"/*jaas.conf || true + +echo "[1/5] Generating broker keystore" +keytool -genkeypair \ + -alias kafka-broker \ + -keyalg RSA \ + -keysize 2048 \ + -validity "$VALIDITY_DAYS" \ + -dname "$DNAME" \ + -ext "$SUBJECT_ALT_NAMES" \ + -keystore "$SECRETS_DIR/kafka.keystore.jks" \ + -storepass "$STOREPASS" \ + -keypass "$STOREPASS" \ + -storetype JKS + +echo "[2/5] Exporting broker certificate" +keytool -exportcert \ + -alias kafka-broker \ + -keystore "$SECRETS_DIR/kafka.keystore.jks" \ + -storepass "$STOREPASS" \ + -rfc \ + -file "$SECRETS_DIR/kafka-broker.crt" + +echo "[3/5] Creating shared truststore" +keytool -importcert \ + -alias kafka-broker \ + -file "$SECRETS_DIR/kafka-broker.crt" \ + -keystore "$SECRETS_DIR/kafka.truststore.jks" \ + -storepass "$TRUSTPASS" \ + -noprompt + +echo "[4/5] Creating client truststores" +cp "$SECRETS_DIR/kafka.truststore.jks" "$SECRETS_DIR/connect.truststore.jks" +cp "$SECRETS_DIR/kafka.truststore.jks" "$SECRETS_DIR/backend.truststore.jks" + +echo "[5/5] Writing credential helper files" +printf '%s\n' "$STOREPASS" > "$SECRETS_DIR/kafka_keystore_creds" +printf '%s\n' "$STOREPASS" > "$SECRETS_DIR/kafka_sslkey_creds" +printf '%s\n' "$TRUSTPASS" > "$SECRETS_DIR/kafka_truststore_creds" + +chmod 600 \ + "$SECRETS_DIR/kafka_keystore_creds" \ + "$SECRETS_DIR/kafka_sslkey_creds" \ + "$SECRETS_DIR/kafka_truststore_creds" + +cat > "$SECRETS_DIR/kafka_server_jaas.conf" < "$SECRETS_DIR/kafka_admin_client.properties" </dev/null 2>&1; then + echo " [FAIL] service not found: $svc" + failed=1 + continue + fi + + status="$(docker compose ps --format json "$svc" | tr -d '\n' | sed 's/}\s*{/}\n{/g' | grep -m1 '"Service":"'"$svc"'"' || true)" + if [[ -z "$status" ]]; then + echo " [FAIL] unable to read status: $svc" + failed=1 + continue + fi + + if echo "$status" | grep -q '"State":"running"'; then + echo " [OK] $svc running" + else + echo " [FAIL] $svc not running" + failed=1 + fi +done + +echo "[2/4] Verifying secure env in backend" +backend_env="$(docker compose exec -T backend /bin/sh -lc 'env' 2>/dev/null || true)" +if echo "$backend_env" | grep -q '^KAFKA_SECURITY_PROTOCOL=SASL_SSL$'; then + echo " [OK] backend KAFKA_SECURITY_PROTOCOL=SASL_SSL" +else + echo " [FAIL] backend missing KAFKA_SECURITY_PROTOCOL=SASL_SSL" + failed=1 +fi + +echo "[3/4] Verifying secure env in kafka-connect" +connect_env="$(docker compose exec -T kafka-connect /bin/sh -lc 'env' 2>/dev/null || true)" +if echo "$connect_env" | grep -q '^CONNECT_SECURITY_PROTOCOL=SASL_SSL$'; then + echo " [OK] connect CONNECT_SECURITY_PROTOCOL=SASL_SSL" +else + echo " [FAIL] connect missing CONNECT_SECURITY_PROTOCOL=SASL_SSL" + failed=1 +fi + +echo "[4/4] Scanning logs for obvious auth/ssl failures" +kafka_logs="$(docker compose logs --tail=200 kafka 2>/dev/null || true)" +connect_logs="$(docker compose logs --tail=200 kafka-connect 2>/dev/null || true)" +backend_logs="$(docker compose logs --tail=200 backend 2>/dev/null || true)" +combined_logs="$kafka_logs +$connect_logs +$backend_logs" + +if echo "$combined_logs" | grep -Eiq 'SSLHandshakeException|SaslAuthenticationException|PKIX path building failed|No route to host|Connection refused'; then + echo " [WARN] suspicious errors detected in recent logs" + echo " inspect with: docker compose logs --tail=200 kafka kafka-connect backend" + failed=1 +else + echo " [OK] no obvious TLS/SASL errors in recent logs" +fi + +if [[ "$failed" -ne 0 ]]; then + echo "Secure smoke test FAILED" + exit 1 +fi + +echo "Secure smoke test PASSED" diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e21fea2..e705e67 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -26,6 +26,13 @@ spring: auto-offset-reset: earliest key-deserializer: org.apache.kafka.common.serialization.StringDeserializer value-deserializer: org.apache.kafka.common.serialization.StringDeserializer + properties: + security.protocol: ${KAFKA_SECURITY_PROTOCOL:PLAINTEXT} + sasl.mechanism: ${KAFKA_SASL_MECHANISM:} + sasl.jaas.config: ${KAFKA_SASL_JAAS_CONFIG:} + ssl.truststore.location: ${KAFKA_SSL_TRUSTSTORE_LOCATION:} + ssl.truststore.password: ${KAFKA_SSL_TRUSTSTORE_PASSWORD:} + ssl.endpoint.identification.algorithm: ${KAFKA_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM:HTTPS} # ─── Mail (SMTP with TLS – CWE-319: no cleartext transmission) ────────────── mail: