#!/usr/bin/env python3 """Rsyslog Centralization Agent - Generates and deploys TLS-secured rsyslog configurations.""" import json import logging import argparse import subprocess from datetime import datetime from jinja2 import Template logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger(__name__) SERVER_TEMPLATE = Template("""\ # Rsyslog Server Configuration - TLS Centralized Logging # Generated by Syslog Centralization Agent # Load modules module(load="imuxsock") module(load="imklog") module(load="imtcp" StreamDriver.Name="gtls" StreamDriver.Mode="1" StreamDriver.Authmode="x509/name" PermittedPeer=["{{ permitted_peers | join('","') }}"] ) # TLS Certificate Configuration global( DefaultNetstreamDriver="gtls" DefaultNetstreamDriverCAFile="{{ ca_cert }}" DefaultNetstreamDriverCertFile="{{ server_cert }}" DefaultNetstreamDriverKeyFile="{{ server_key }}" ) # TLS Listener input(type="imtcp" port="{{ tls_port }}") # Templates template(name="PerHostDir" type="string" string="/var/log/remote/%HOSTNAME%/%PROGRAMNAME%.log") template(name="JSONFormat" type="string" string='{"timestamp":"%TIMESTAMP:::date-rfc3339%","host":"%HOSTNAME%","facility":"%syslogfacility-text%","severity":"%syslogseverity-text%","program":"%PROGRAMNAME%","msg":"%msg:::json%"}\\n') template(name="PerHostJSON" type="string" string="/var/log/remote/%HOSTNAME%/json/%PROGRAMNAME%.json") # Rules - Store per-host with standard format *.* ?PerHostDir # Also store in JSON format for SIEM ingestion *.* ?PerHostJSON;JSONFormat # High-severity alerts to dedicated file *.err /var/log/remote/errors.log """) CLIENT_TEMPLATE = Template("""\ # Rsyslog Client Configuration - TLS Forwarding # Generated by Syslog Centralization Agent # TLS Certificate Configuration global( DefaultNetstreamDriver="gtls" DefaultNetstreamDriverCAFile="{{ ca_cert }}" DefaultNetstreamDriverCertFile="{{ client_cert }}" DefaultNetstreamDriverKeyFile="{{ client_key }}" ) # Forward all logs to central server with TLS and reliable queue action( type="omfwd" target="{{ server_ip }}" port="{{ tls_port }}" protocol="tcp" StreamDriver="gtls" StreamDriverMode="1" StreamDriverAuthMode="x509/name" StreamDriverPermittedPeers="{{ server_ip }}" queue.type="LinkedList" queue.filename="fwdRule1" queue.maxdiskspace="{{ queue_disk_space }}" queue.saveonshutdown="on" queue.size="{{ queue_size }}" action.resumeRetryCount="-1" action.resumeInterval="30" ) """) def generate_server_config(server_ip, clients, ca_cert, server_cert, server_key, tls_port=6514): """Generate rsyslog server configuration with TLS.""" config = SERVER_TEMPLATE.render( permitted_peers=clients + [server_ip], ca_cert=ca_cert, server_cert=server_cert, server_key=server_key, tls_port=tls_port, ) logger.info("Generated server config for %s with %d permitted peers", server_ip, len(clients)) return config def generate_client_config(server_ip, ca_cert, client_cert, client_key, tls_port=6514): """Generate rsyslog client configuration with TLS forwarding.""" config = CLIENT_TEMPLATE.render( server_ip=server_ip, ca_cert=ca_cert, client_cert=client_cert, client_key=client_key, tls_port=tls_port, queue_disk_space="1g", queue_size="50000", ) logger.info("Generated client config forwarding to %s:%d", server_ip, tls_port) return config def generate_tls_certificates(output_dir, server_cn, client_cns): """Generate CA, server, and client TLS certificates using OpenSSL.""" ca_key = f"{output_dir}/ca-key.pem" ca_cert = f"{output_dir}/ca.pem" subprocess.run([ "openssl", "req", "-x509", "-newkey", "rsa:4096", "-keyout", ca_key, "-out", ca_cert, "-days", "3650", "-nodes", "-subj", f"/CN=Syslog CA/O=SOC/C=US", ], capture_output=True, check=True) logger.info("Generated CA certificate: %s", ca_cert) for cn in [server_cn] + client_cns: key_file = f"{output_dir}/{cn}-key.pem" cert_file = f"{output_dir}/{cn}-cert.pem" csr_file = f"{output_dir}/{cn}.csr" subprocess.run([ "openssl", "req", "-newkey", "rsa:2048", "-keyout", key_file, "-out", csr_file, "-nodes", "-subj", f"/CN={cn}/O=SOC/C=US", ], capture_output=True, check=True) subprocess.run([ "openssl", "x509", "-req", "-in", csr_file, "-CA", ca_cert, "-CAkey", ca_key, "-CAcreateserial", "-out", cert_file, "-days", "365", ], capture_output=True, check=True) logger.info("Generated certificate for %s", cn) return ca_cert def deploy_config_ssh(host, config_content, remote_path, username="root", key_file=None): """Deploy rsyslog configuration to a remote host via SSH.""" import paramiko client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) connect_kwargs = {"hostname": host, "username": username} if key_file: connect_kwargs["key_filename"] = key_file client.connect(**connect_kwargs) sftp = client.open_sftp() with sftp.file(remote_path, "w") as f: f.write(config_content) sftp.close() _, stdout, stderr = client.exec_command("systemctl restart rsyslog") exit_status = stdout.channel.recv_exit_status() client.close() logger.info("Deployed config to %s:%s (restart exit: %d)", host, remote_path, exit_status) return exit_status == 0 def validate_tls_connection(server_ip, tls_port=6514, ca_cert=None): """Validate TLS connectivity to the rsyslog server.""" cmd = [ "openssl", "s_client", "-connect", f"{server_ip}:{tls_port}", "-CAfile", ca_cert or "/etc/ssl/certs/ca-certificates.crt", ] try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=10, input="") connected = "Verify return code: 0" in result.stdout logger.info("TLS validation to %s:%d: %s", server_ip, tls_port, "OK" if connected else "FAILED") return connected except subprocess.TimeoutExpired: return False def generate_report(server_config, client_configs, deployments, tls_valid): """Generate syslog centralization deployment report.""" report = { "timestamp": datetime.utcnow().isoformat(), "server_config_generated": bool(server_config), "client_configs_generated": len(client_configs), "deployments": deployments, "tls_validated": tls_valid, } print(f"SYSLOG REPORT: {len(client_configs)} client configs, TLS: {'OK' if tls_valid else 'PENDING'}") return report def main(): parser = argparse.ArgumentParser(description="Rsyslog Centralization Agent") parser.add_argument("--server-ip", required=True, help="Syslog server IP") parser.add_argument("--clients", required=True, help="Comma-separated client IPs") parser.add_argument("--ca-cert", default="/etc/rsyslog.d/ca.pem") parser.add_argument("--server-cert", default="/etc/rsyslog.d/server-cert.pem") parser.add_argument("--server-key", default="/etc/rsyslog.d/server-key.pem") parser.add_argument("--tls-port", type=int, default=6514) parser.add_argument("--deploy", action="store_true", help="Deploy configs via SSH") parser.add_argument("--config-dir", default="./rsyslog_configs") parser.add_argument("--output", default="syslog_report.json") args = parser.parse_args() clients = [c.strip() for c in args.clients.split(",")] import os os.makedirs(args.config_dir, exist_ok=True) server_config = generate_server_config( args.server_ip, clients, args.ca_cert, args.server_cert, args.server_key, args.tls_port ) with open(f"{args.config_dir}/server.conf", "w") as f: f.write(server_config) client_configs = {} for client_ip in clients: config = generate_client_config( args.server_ip, args.ca_cert, f"/etc/rsyslog.d/{client_ip}-cert.pem", f"/etc/rsyslog.d/{client_ip}-key.pem", args.tls_port, ) with open(f"{args.config_dir}/client-{client_ip}.conf", "w") as f: f.write(config) client_configs[client_ip] = config deployments = [] if args.deploy: for client_ip, config in client_configs.items(): ok = deploy_config_ssh(client_ip, config, "/etc/rsyslog.d/99-central.conf") deployments.append({"host": client_ip, "success": ok}) tls_valid = validate_tls_connection(args.server_ip, args.tls_port, args.ca_cert) report = generate_report(server_config, client_configs, deployments, tls_valid) with open(args.output, "w") as f: json.dump(report, f, indent=2) logger.info("Report saved to %s", args.output) if __name__ == "__main__": main()