Skip to content

Commit 3670765

Browse files
author
BESS Solutions
committed
feat(security): v2.1.0 — IEC 62443 GAP-003 CLOSED: mTLS OT segment (SR 3.1)
Implement mutual TLS for Modbus TCP OT segment — all 3 IEC 62443 gaps now CLOSED. SL-2 readiness: ~65% → ~85% Component 1 — PKI / Certificates: - infrastructure/certs/gen_certs.sh: openssl script for CA + gateway client + stunnel proxy certs - .gitignore: exclude *.key, *.pem, *.srl from commits (private keys MUST NOT be committed) Component 2 — stunnel mTLS proxy: - infrastructure/docker/stunnel-ot.conf: stunnel client config (TLS 1.3, verify=2, ECDHE ciphers) - infrastructure/docker/docker-compose.yml: add bessai-stunnel service (profile: ot-security) Architecture: Gateway → TCP:502 (bess-net) → stunnel → TLS 1.3:8502 → Inversor BESS Component 3 — UniversalDriver TLS native support: - src/interfaces/ot_tls_config.py: OtTlsConfig.from_env() + build_ssl_context() Env vars: OT_MTLS_ENABLED, OT_CA_CERT_PATH, OT_CLIENT_CERT_PATH, OT_CLIENT_KEY_PATH - src/drivers/modbus_driver.py: optional tls_context/tls_ca_cert/tls_client_cert/tls_client_key params in UniversalDriver.__init__() — fully backwards compatible Tests: - tests/test_ot_tls_config.py: 9 passed, 1 skipped (openssl not in PATH on Windows CI) - Suite: 419 passed, 5 skipped — 0 failures, 0 errors (+9 vs v2.0.0) Documentation: - docs/compliance/iec_62443_sl2_certification_path.md: GAP-001/002/003 marked CLOSED, readiness ~85%
1 parent dd3e520 commit 3670765

8 files changed

Lines changed: 721 additions & 20 deletions

File tree

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,15 @@ out/
104104
.DS_Store
105105
Thumbs.db
106106
desktop.ini
107+
108+
# ---------------------------------------------------------------------------
109+
# IEC 62443 GAP-003: OT PKI — private keys MUST NOT be committed
110+
# Run: bash infrastructure/certs/gen_certs.sh to regenerate locally
111+
# ---------------------------------------------------------------------------
112+
infrastructure/certs/*.key
113+
infrastructure/certs/*.pem
114+
infrastructure/certs/*.srl
115+
# CA key is especially sensitive
116+
infrastructure/certs/ca.key
117+
# Public certs are safe to commit (no secret material)
118+
# infrastructure/certs/*.crt ← intentionally NOT ignored

docs/compliance/iec_62443_sl2_certification_path.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ Based on the existing `iec62443_sl2_gap.md` analysis:
2020

2121
| Domain | Current Status | Gap |
2222
|---|---|---|
23-
| Authentication (SR 1.1) | API key auth implemented | Needs MFA for management access |
24-
| Audit logging (SR 2.8) | Structured logs via structlog | Log forwarding to SIEM needed |
23+
| Authentication (SR 1.1) | API key auth implemented | MFA via TOTP (GAP-001 CLOSED v2.0.0) |
24+
| Audit logging (SR 2.8) | Structured logs via structlog | ✅ Loki SIEM forwarding (GAP-002 CLOSED v2.0.0) |
2525
| Software integrity (SR 3.2) | SLSA Level 2 + cosign | Not formally evaluated by auditor |
2626
| Vulnerability disclosure (SR 2.12) | `SECURITY.md` exists | No formal PSIRT process |
2727
| Patch management (SR 2.2) | Dependabot + CI | Formal patch SLA document needed |
28-
| Communication integrity (SR 3.1) | TLS enforced (Ingress) | mTLS for OT segment not implemented |
28+
| Communication integrity (SR 3.1) | TLS enforced (Ingress) | mTLS OT segment (GAP-003 CLOSED v2.1.0) |
2929
| Network segmentation (SR 5.2) | Docker network isolation | Formal network diagram needed |
3030

31-
**Estimated SL-2 readiness:** ~65%
31+
**Estimated SL-2 readiness:** ~85% *(was 65% before v2.0.0–v2.1.0)*
3232

3333
---
3434

@@ -49,12 +49,12 @@ Based on the existing `iec62443_sl2_gap.md` analysis:
4949

5050
**Duration:** 6–10 weeks (parallel to Phase 1 tail)
5151

52-
| Gap | Remediation | Owner |
53-
|---|---|---|
54-
| MFA for management | Implement TOTP for admin dashboard via `pyotp` | Engineering |
55-
| SIEM log forwarding | Add Fluentd/Loki output target for audit logs | Engineering |
56-
| mTLS for OT segment | Add mutual TLS config to Modbus TCP wrapper | Engineering |
57-
| Formal network diagram | Create `docs/architecture/network_diagram.md` with OT zones | Engineering |
52+
| Gap | Remediation | Owner | Status |
53+
|---|---|---|---|
54+
| MFA for management | TOTP for admin dashboard via `pyotp` | Engineering | ✅ CLOSED v2.0.0 |
55+
| SIEM log forwarding | Loki output via OTel Collector | Engineering | ✅ CLOSED v2.0.0 |
56+
| mTLS for OT segment | stunnel proxy + `ot_tls_config.py` + `gen_certs.sh` | Engineering | ✅ CLOSED v2.1.0 |
57+
| Formal network diagram | Create `docs/architecture/network_diagram.md` with OT zones | Engineering | 🔲 Pending |
5858

5959
### Phase 3 — Formal Audit (Q2–Q3 2026)
6060

infrastructure/certs/gen_certs.sh

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env bash
2+
# =============================================================================
3+
# BESSAI Edge Gateway — OT PKI Certificate Generator
4+
# IEC 62443-3-3 SR 3.1: Communication Integrity via mutual TLS
5+
# GAP-003 REMEDIATION
6+
# =============================================================================
7+
# Usage:
8+
# bash infrastructure/certs/gen_certs.sh
9+
#
10+
# Output (in infrastructure/certs/):
11+
# ca.key — CA private key (SECRET — never commit)
12+
# ca.crt — CA root certificate (safe to commit)
13+
# gateway-client.key — Gateway private key (SECRET — never commit)
14+
# gateway-client.crt — Gateway certificate (safe to commit)
15+
# gateway-client.csr — CSR (intermediate) (safe to commit/discard)
16+
# modbus-proxy.key — Proxy private key (SECRET — never commit)
17+
# modbus-proxy.crt — Proxy certificate (safe to commit)
18+
# modbus-proxy.csr — CSR (intermediate) (safe to commit/discard)
19+
#
20+
# Requirements: openssl (ships with Git for Windows / WSL / macOS / Linux)
21+
# =============================================================================
22+
23+
set -euo pipefail
24+
25+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26+
OUT_DIR="$SCRIPT_DIR"
27+
28+
# Certificate validity — 10 years (edge devices have infrequent rotations)
29+
VALIDITY_DAYS=3650
30+
31+
# Organization info (customize per deployment)
32+
COUNTRY="${BESSAI_CERT_COUNTRY:-CL}"
33+
ORG="${BESSAI_CERT_ORG:-BESSAI-Solutions}"
34+
SITE_ID="${BESSAI_SITE_ID:-edge-001}"
35+
36+
echo "════════════════════════════════════════════════════"
37+
echo " BESSAI OT PKI Generator — IEC 62443 GAP-003"
38+
echo " Site: ${SITE_ID} | Org: ${ORG}"
39+
echo "════════════════════════════════════════════════════"
40+
41+
# ── 1. CA Root (BESSAI-OT-CA) ─────────────────────────────────────────────────
42+
echo ""
43+
echo "[1/3] Generating BESSAI-OT-CA root certificate..."
44+
45+
openssl genrsa -out "${OUT_DIR}/ca.key" 4096 2>/dev/null
46+
47+
openssl req -new -x509 \
48+
-key "${OUT_DIR}/ca.key" \
49+
-out "${OUT_DIR}/ca.crt" \
50+
-days "${VALIDITY_DAYS}" \
51+
-subj "/C=${COUNTRY}/O=${ORG}/CN=BESSAI-OT-CA-${SITE_ID}" \
52+
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
53+
-addext "keyUsage=critical,keyCertSign,cRLSign"
54+
55+
echo " ✅ CA: ${OUT_DIR}/ca.crt"
56+
57+
# ── 2. Gateway Client Certificate ─────────────────────────────────────────────
58+
echo ""
59+
echo "[2/3] Generating gateway client certificate..."
60+
61+
openssl genrsa -out "${OUT_DIR}/gateway-client.key" 2048 2>/dev/null
62+
63+
openssl req -new \
64+
-key "${OUT_DIR}/gateway-client.key" \
65+
-out "${OUT_DIR}/gateway-client.csr" \
66+
-subj "/C=${COUNTRY}/O=${ORG}/CN=bessai-gateway-${SITE_ID}"
67+
68+
openssl x509 -req \
69+
-in "${OUT_DIR}/gateway-client.csr" \
70+
-CA "${OUT_DIR}/ca.crt" \
71+
-CAkey "${OUT_DIR}/ca.key" \
72+
-CAcreateserial \
73+
-out "${OUT_DIR}/gateway-client.crt" \
74+
-days "${VALIDITY_DAYS}" \
75+
-extfile <(printf "extendedKeyUsage=clientAuth\nsubjectAltName=DNS:bessai-gateway,DNS:localhost") \
76+
2>/dev/null
77+
78+
echo " ✅ Gateway client cert: ${OUT_DIR}/gateway-client.crt"
79+
80+
# ── 3. Modbus Proxy (stunnel) Server Certificate ──────────────────────────────
81+
echo ""
82+
echo "[3/3] Generating modbus-proxy (stunnel) server certificate..."
83+
84+
openssl genrsa -out "${OUT_DIR}/modbus-proxy.key" 2048 2>/dev/null
85+
86+
openssl req -new \
87+
-key "${OUT_DIR}/modbus-proxy.key" \
88+
-out "${OUT_DIR}/modbus-proxy.csr" \
89+
-subj "/C=${COUNTRY}/O=${ORG}/CN=bessai-modbus-proxy-${SITE_ID}"
90+
91+
openssl x509 -req \
92+
-in "${OUT_DIR}/modbus-proxy.csr" \
93+
-CA "${OUT_DIR}/ca.crt" \
94+
-CAkey "${OUT_DIR}/ca.key" \
95+
-CAcreateserial \
96+
-out "${OUT_DIR}/modbus-proxy.crt" \
97+
-days "${VALIDITY_DAYS}" \
98+
-extfile <(printf "extendedKeyUsage=serverAuth\nsubjectAltName=DNS:bessai-stunnel,DNS:localhost") \
99+
2>/dev/null
100+
101+
echo " ✅ Proxy server cert: ${OUT_DIR}/modbus-proxy.crt"
102+
103+
# ── Summary ───────────────────────────────────────────────────────────────────
104+
echo ""
105+
echo "════════════════════════════════════════════════════"
106+
echo " PKI generation complete."
107+
echo ""
108+
echo " Certificates generated in: ${OUT_DIR}"
109+
echo ""
110+
echo " ⚠️ PRIVATE KEYS — DO NOT COMMIT:"
111+
echo " ca.key gateway-client.key modbus-proxy.key"
112+
echo ""
113+
echo " Next steps:"
114+
echo " 1. docker compose --profile ot-security up -d"
115+
echo " 2. Set in .env:"
116+
echo " OT_MTLS_ENABLED=true"
117+
echo " OT_CA_CERT_PATH=infrastructure/certs/ca.crt"
118+
echo " OT_CLIENT_CERT_PATH=infrastructure/certs/gateway-client.crt"
119+
echo " OT_CLIENT_KEY_PATH=infrastructure/certs/gateway-client.key"
120+
echo "════════════════════════════════════════════════════"

infrastructure/docker/docker-compose.yml

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@
88
# prometheus — Metrics scraper (monitoring profile)
99
# grafana — Metrics + Logs dashboard (monitoring profile)
1010
# bessai-loki — Grafana Loki SIEM (monitoring profile, IEC 62443 GAP-002)
11+
# bessai-stunnel — mTLS proxy OT segment (ot-security profile, IEC 62443 GAP-003)
1112
#
1213
# Profiles:
13-
# (default) — gateway + otel-collector (requires real INVERTER_IP in .env)
14-
# simulator — modbus-simulator + gateway-sim + otel-collector (dev mode)
15-
# monitoring — prometheus + grafana + loki (add to any profile for dashboards)
16-
# Set LOKI_ENDPOINT=http://bessai-loki:3100/loki/api/v1/push
14+
# (default) — gateway + otel-collector (requires real INVERTER_IP in .env)
15+
# simulator — modbus-simulator + gateway-sim + otel-collector (dev mode)
16+
# monitoring — prometheus + grafana + loki (add to any profile for dashboards)
17+
# Set LOKI_ENDPOINT=http://bessai-loki:3100/loki/api/v1/push
18+
# ot-security — stunnel mTLS proxy (IEC 62443 SR 3.1 — Communication Integrity)
19+
# Requires: bash infrastructure/certs/gen_certs.sh (first time)
20+
# Set: OT_MTLS_ENABLED=true OT_CA_CERT_PATH=... (in .env)
1721
#
1822
# Usage:
1923
# # Development mode (with Modbus simulator):
@@ -24,6 +28,10 @@
2428
# docker compose -f infrastructure/docker/docker-compose.yml \
2529
# --profile simulator --profile monitoring up --build
2630
#
31+
# # Production with mTLS (IEC 62443 GAP-003):
32+
# docker compose -f infrastructure/docker/docker-compose.yml \
33+
# --profile ot-security up -d
34+
#
2735
# # Access:
2836
# # /health → http://localhost:8000/health
2937
# # /metrics → http://localhost:8000/metrics
@@ -43,7 +51,8 @@ volumes:
4351
otel-data:
4452
prometheus-data:
4553
grafana-data:
46-
loki-data: # IEC 62443 GAP-002: SIEM audit log persistence
54+
loki-data:
55+
# IEC 62443 GAP-002: SIEM audit log persistence
4756

4857
# ── Services ──────────────────────────────────────────────────────────────────
4958
services:
@@ -90,7 +99,7 @@ services:
9099
- "8000:8000" # /health and /metrics HTTP endpoints
91100
- "8080:8080" # Dashboard UI → http://localhost:8080
92101
volumes:
93-
- ../../dashboard:/app/dashboard:ro # serve static assets
102+
- ../../dashboard:/app/dashboard:ro # serve static assets
94103
networks:
95104
- bess-net
96105
depends_on:
@@ -195,7 +204,7 @@ services:
195204
- bess-net
196205
depends_on:
197206
- prometheus
198-
- bessai-loki # IEC 62443 GAP-002: Loki datasource
207+
- bessai-loki # IEC 62443 GAP-002: Loki datasource
199208
logging:
200209
driver: "json-file"
201210
options:
@@ -216,7 +225,7 @@ services:
216225
- ../loki/loki-config.yaml:/etc/loki/loki-config.yaml:ro
217226
- loki-data:/loki
218227
ports:
219-
- "3100:3100" # Loki push API + query API
228+
- "3100:3100" # Loki push API + query API
220229
networks:
221230
- bess-net
222231
healthcheck:
@@ -230,3 +239,45 @@ services:
230239
options:
231240
max-size: "5m"
232241
max-file: "2"
242+
243+
# ── bessai-stunnel — mTLS proxy OT segment (ot-security profile) ────────────
244+
# IEC 62443-3-3 SR 3.1: Communication Integrity — GAP-003 CLOSED
245+
#
246+
# Architecture:
247+
# Gateway → TCP:502 (plain, inside bess-net) → stunnel → TLS 1.3 → Inversor
248+
#
249+
# Prerequisites (first time):
250+
# bash infrastructure/certs/gen_certs.sh
251+
#
252+
# Required env vars (.env):
253+
# INVERTER_IP — IP of the real BESS inverter (e.g. 192.168.1.100)
254+
# OT_MTLS_ENABLED=true — activates TLS path in UniversalDriver
255+
bessai-stunnel:
256+
image: dweomer/stunnel:5.72
257+
container_name: bessai-stunnel
258+
profiles: [ "ot-security" ]
259+
restart: unless-stopped
260+
environment:
261+
INVERTER_IP: "${INVERTER_IP:-192.168.1.100}"
262+
volumes:
263+
# TLS certificates (generated by gen_certs.sh — keys are gitignored)
264+
- ../../infrastructure/certs/gateway-client.crt:/etc/stunnel/gateway-client.crt:ro
265+
- ../../infrastructure/certs/gateway-client.key:/etc/stunnel/gateway-client.key:ro
266+
- ../../infrastructure/certs/ca.crt:/etc/stunnel/ca.crt:ro
267+
# stunnel OT configuration
268+
- ./stunnel-ot.conf:/etc/stunnel/stunnel.conf:ro
269+
ports:
270+
- "502:502" # Modbus TCP plain inbound — only reachable within bess-net
271+
networks:
272+
- bess-net
273+
healthcheck:
274+
test: [ "CMD", "sh", "-c", "kill -0 $(cat /var/run/stunnel4/stunnel.pid 2>/dev/null) 2>/dev/null || exit 1" ]
275+
interval: 30s
276+
timeout: 10s
277+
retries: 3
278+
start_period: 5s
279+
logging:
280+
driver: "json-file"
281+
options:
282+
max-size: "5m"
283+
max-file: "2"
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
; =============================================================================
2+
; BESSAI Edge Gateway — stunnel mTLS proxy for OT segment
3+
; IEC 62443-3-3 SR 3.1: Communication Integrity
4+
; GAP-003 REMEDIATION
5+
; =============================================================================
6+
;
7+
; Architecture:
8+
; Gateway (Python, Modbus TCP plain)
9+
; → bessai-stunnel:502 (TCP, plain, inside bess-net — trusted)
10+
; → INVERTER_IP:8502 (TLS 1.3, mutual auth)
11+
;
12+
; The gateway never handles TLS itself — the proxy is fully transparent.
13+
; Only the stunnel container holds the private key material.
14+
;
15+
; Run:
16+
; docker compose --profile ot-security up -d bessai-stunnel
17+
; =============================================================================
18+
19+
; ── Global settings ──────────────────────────────────────────────────────────
20+
pid = /var/run/stunnel4/stunnel.pid
21+
foreground = yes
22+
output = /dev/stdout
23+
24+
; Minimum TLS version: 1.2 (1.3 preferred; IEC 62443 SR 3.1 requires ≥ TLS 1.2)
25+
sslVersion = TLSv1.3
26+
27+
; Cipher suites — prefer forward secrecy (ECDHE)
28+
ciphers = ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256
29+
30+
; ── OT Modbus service ─────────────────────────────────────────────────────────
31+
[modbus-ot]
32+
33+
; Client mode: stunnel connects OUT to the inverter, wrapping in TLS
34+
client = yes
35+
36+
; Accept plain Modbus TCP from the gateway (inside Docker bess-net only)
37+
accept = 0.0.0.0:502
38+
39+
; Forward to the real inverter over mTLS
40+
; INVERTER_IP is substituted by the Docker env var at container start
41+
connect = ${INVERTER_IP}:8502
42+
43+
; — Client identity (gateway certificate) —
44+
cert = /etc/stunnel/gateway-client.crt
45+
key = /etc/stunnel/gateway-client.key
46+
47+
; — CA to validate the inverter server certificate —
48+
CAfile = /etc/stunnel/ca.crt
49+
50+
; Strict server cert verification (verify=2 checks cert + CA chain)
51+
verify = 2
52+
53+
; Connection timeout (seconds)
54+
TIMEOUTconnect = 10
55+
TIMEOUTidle = 300

0 commit comments

Comments
 (0)