Netdata.cloud bot for Zulip

feat: hardcode Netdata CA certificate for mutual TLS

- Add hardcoded Netdata CA certificate from official documentation
- Remove client_ca_path configuration parameter
- Update server to use built-in certificate for MTLS validation
- Simplify configuration by eliminating need for external CA file
- Update all documentation and config examples

The Netdata CA certificate is now embedded directly in the code,
eliminating the need for users to configure and manage the CA file
separately. This improves security and simplifies deployment.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

-13
.env.sample
···
-
# Zulip Configuration
-
ZULIP_SITE=https://yourorg.zulipchat.com
-
ZULIP_EMAIL=netdata-bot@yourorg.zulipchat.com
-
ZULIP_API_KEY=your-api-key-here
-
ZULIP_STREAM=netdata-alerts
-
-
# Server Configuration
-
SERVER_HOST=0.0.0.0
-
SERVER_PORT=8443
-
SERVER_DOMAIN=your-domain.com
-
SERVER_CERT_PATH=/etc/letsencrypt/live
-
SERVER_ENABLE_MTLS=true
-
SERVER_CLIENT_CA_PATH=/path/to/netdata-client-ca.pem
-1
CLAUDE.md
···
- `SERVER_DOMAIN`: Public domain for Let's Encrypt
- `SERVER_PORT`: HTTPS port (default: 8443)
- `SERVER_ENABLE_MTLS`: Enable mutual TLS
-
- `SERVER_CLIENT_CA_PATH`: Path to Netdata client CA
### Message Processing
1. Receive Netdata webhook POST request
+1 -2
README.md
···
- `SERVER_PORT`: HTTPS port (default: `8443`)
- `SERVER_CERT_PATH`: Certificate path (default: `/etc/letsencrypt/live`)
- `SERVER_ENABLE_MTLS`: Enable mutual TLS (default: `true`)
-
- `SERVER_CLIENT_CA_PATH`: Client CA certificate for mTLS validation
## Message Format
···
1. **Server Certificate**: Automatically managed by Let's Encrypt
2. **Client Verification**: Validates Netdata's client certificate
-
3. **CA Certificate**: Configure `SERVER_CLIENT_CA_PATH` to validate client certs
+
3. **CA Certificate**: Built-in Netdata CA certificate for client validation
### Webhook Endpoint Security
-3
docker-compose.yml
···
- SERVER_HOST=0.0.0.0
- SERVER_CERT_PATH=/etc/letsencrypt/live
- SERVER_ENABLE_MTLS=true
-
- SERVER_CLIENT_CA_PATH=/etc/ssl/certs/netdata-ca.pem
# Zulip configuration
- ZULIP_SITE=https://yourorg.zulipchat.com
···
# Mount Let's Encrypt certificates
- /etc/letsencrypt/live:/etc/letsencrypt/live:ro
- /etc/letsencrypt/archive:/etc/letsencrypt/archive:ro
-
# Mount CA certificate for mutual TLS (optional)
-
- /path/to/netdata-ca.pem:/etc/ssl/certs/netdata-ca.pem:ro
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-k", "-f", "https://localhost:8443/health"]
-1
examples/netdata-zulip-bot.service
···
Environment=SERVER_PORT=8443
Environment=SERVER_CERT_PATH=/etc/letsencrypt/live
Environment=SERVER_ENABLE_MTLS=true
-
Environment=SERVER_CLIENT_CA_PATH=/etc/ssl/certs/netdata-ca.pem
# Zulip configuration (if using environment variables instead of zuliprc)
# Environment=ZULIP_SITE=https://yourorg.zulipchat.com
-1
netdata_zulip_bot/config.py
···
domain=os.getenv("SERVER_DOMAIN", ""),
cert_path=os.getenv("SERVER_CERT_PATH", "/etc/letsencrypt/live"),
enable_mtls=os.getenv("SERVER_ENABLE_MTLS", "true").lower() == "true",
-
client_ca_path=os.getenv("SERVER_CLIENT_CA_PATH"),
)
# Validate required server settings
-1
netdata_zulip_bot/main.py
···
SERVER_DOMAIN=your-domain.com
SERVER_CERT_PATH=/etc/letsencrypt/live
SERVER_ENABLE_MTLS=true
-
SERVER_CLIENT_CA_PATH=/path/to/netdata-client-ca.pem
"""
with open(".env.sample", 'w') as f:
-1
netdata_zulip_bot/models.py
···
domain: str # Required for Let's Encrypt
cert_path: str = "/etc/letsencrypt/live"
enable_mtls: bool = True
-
client_ca_path: Optional[str] = None
model_config = ConfigDict(env_prefix="SERVER_")
+38
netdata_zulip_bot/netdata_ca.py
···
+
"""Netdata Cloud CA certificate for mutual TLS authentication."""
+
+
# This certificate is from the official Netdata documentation:
+
# https://github.com/netdata/netdata/blob/master/integrations/cloud-notifications/metadata.yaml
+
NETDATA_CA_CERT = """-----BEGIN CERTIFICATE-----
+
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
+
5zrbwvQf
+
-----END CERTIFICATE-----"""
+14 -12
netdata_zulip_bot/server.py
···
"""FastAPI webhook server for receiving Netdata notifications."""
import ssl
+
import tempfile
from pathlib import Path
from typing import Dict, Any
···
from .formatter import ZulipMessageFormatter
from .models import WebhookPayload, ZulipConfig, ServerConfig
+
from .netdata_ca import NETDATA_CA_CERT
from .zulip_client import ZulipNotifier
logger = structlog.get_logger()
···
# Configure mutual TLS if enabled
if self.server_config.enable_mtls:
-
if self.server_config.client_ca_path:
-
ca_path = Path(self.server_config.client_ca_path)
-
if ca_path.exists():
-
context.load_verify_locations(str(ca_path))
-
context.verify_mode = ssl.CERT_REQUIRED
-
logger.info("Mutual TLS enabled", ca_path=str(ca_path))
-
else:
-
logger.warning("Client CA file not found, disabling mutual TLS", ca_path=str(ca_path))
-
context.verify_mode = ssl.CERT_NONE
-
else:
-
logger.warning("No client CA path configured, disabling mutual TLS")
-
context.verify_mode = ssl.CERT_NONE
+
# Use the hardcoded Netdata CA certificate
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.pem', delete=False) as ca_file:
+
ca_file.write(NETDATA_CA_CERT)
+
ca_file_path = ca_file.name
+
+
try:
+
context.load_verify_locations(ca_file_path)
+
context.verify_mode = ssl.CERT_REQUIRED
+
logger.info("Mutual TLS enabled with hardcoded Netdata CA certificate")
+
finally:
+
# Clean up the temporary file
+
Path(ca_file_path).unlink(missing_ok=True)
else:
context.verify_mode = ssl.CERT_NONE
logger.info("Mutual TLS disabled")