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: