Skip to content

Syslog in Practice: A Guide to Golang Security Product Integration ​

Building a production-grade log collection service from scratch — TLS certificate generation, high-performance parsing library, SIEM integration, including a complete runnable Golang demo

Introduction: From Basics to Production ​

at Basics In it, we mastered the syslog protocol format and rsyslog configuration. However, security products in production environments face more severe challenges:

  • Encrypted transmission: Logs contain sensitive information, and transmitting them in plaintext poses an extremely high risk.

  • High performance: Tens of thousands of logs per second—parsing cannot become the bottleneck.

  • Standardization Output format needs to interface with analysis platforms such as SIEM and ELK.

This article will address these issues one by one and deliver Complete, runnable, production-grade code.

1. Security Architecture Design: Centralized Log Collection Topology ​

1.1 Typical Deployment Architecture ​

1.2 Log Pipeline Design Principles for Security Products ​

PrincipleDescriptionImplementation approach
Encrypted transmissionPrevent man-in-the-middle eavesdroppingTLS + mutual certificate authentication
Reliable transmissionAvoid log lossTCP + application layer ACK
Buffer peak shavingHandle burst trafficKafka / Redis buffering
Structured outputConvenient for retrieval and analysisJSON format + field normalization

2. rsyslog receiver configuration: preparing for TLS ​

2.1 Complete production-grade configuration ​

apache
# /etc/rsyslog.conf

# ========== Module Loading ==========
module(load="imtcp")                    # TCP input
module(load="imudp")                    # UDP input (optional)
module(load="imrelp")                   # RELP reliable protocol

# ========== TLS Configuration ==========
global(
    defaultNetstreamDriver="gtls"
    defaultNetstreamDriverCAFile="/etc/rsyslog.d/certs/ca.pem"
    defaultNetstreamDriverCertFile="/etc/rsyslog.d/certs/server-cert.pem"
    defaultNetstreamDriverKeyFile="/etc/rsyslog.d/certs/server-key.pem"
)

# TLS listening (recommended)
input(
    type="imtcp"
    port="5140"
    StreamDriver="gtls"
    StreamDriverMode="1"                # 1=Enable TLS, 0=plaintext
    StreamDriverAuthMode="x509/name"    # Certificate authentication
)

# Plaintext monitoring (compatible with legacy devices; network isolation recommended)
input(type="imtcp" port="514")

# ========== Template: Group into directories by client IP ==========
template(
    name="PerHost"
    type="string"
    string="/var/log/collector/%FROMHOST-IP%/%$YEAR%-%$MONTH%-%$DAY%/%PROGRAMNAME%.log"
)

# ========== Rules ==========
*.*  ?PerHost

2.2 Restart the service and verify ​

bash
# Verify configuration
sudo rsyslogd -f /etc/rsyslog.conf -N1

# Restart the service
sudo systemctl restart rsyslog

# Check listening ports
sudo ss -tlnp | grep 5140
# Output example: LISTEN 0 25 0.0.0.0:5140 0.0.0.0:* users:(("rsyslogd",pid=1234,fd=18))

3. TLS Certificate Generation: Complete Hands-On Guide ​

3.1 Generate a self-signed CA ​

bash
#!/bin/bash
# generate-certs.sh - Generate syslog TLS certificate

# Variable configuration
CA_DIR="./certs"
DAYS=3650
COUNTRY="CN"
STATE="Beijing"
CITY="Beijing"
ORG="SecurityProduct"
ORG_UNIT="LogCollector"

mkdir -p $CA_DIR
cd $CA_DIR

# 1. Generate CA private key and certificate
openssl req -new -x509 -days $DAYS -nodes \
    -newkey rsa:4096 \
    -keyout ca-key.pem \
    -out ca.pem \
    -subj "/C=$COUNTRY/ST=$STATE/L=$CITY/O=$ORG/CN=LogCollector CA"

# 2. Generate the server private key and CSR
openssl req -new -nodes -newkey rsa:4096 \
    -keyout server-key.pem \
    -out server.csr \
    -subj "/C=$COUNTRY/ST=$STATE/L=$CITY/O=$ORG/CN=logcollector.example.com"

# 3. Use the CA to issue a server certificate
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca-key.pem \
    -CAcreateserial -out server-cert.pem -days $DAYS \
    -extfile <(printf "subjectAltName=DNS:localhost,DNS:logcollector.example.com,IP:127.0.0.1")

# 4. Generate a client certificate (for mutual authentication)
openssl req -new -nodes -newkey rsa:4096 \
    -keyout client-key.pem \
    -out client.csr \
    -subj "/C=$COUNTRY/ST=$STATE/L=$CITY/O=$ORG/CN=client"

openssl x509 -req -in client.csr -CA ca.pem -CAkey ca-key.pem \
    -CAcreateserial -out client-cert.pem -days $DAYS

# 5. Clean up temporary files
rm *.csr *.srl

# 6. Set permissions
chmod 600 *.key
chmod 644 *.pem

echo "✅ Certificate generation complete!"
ls -la

3.2 Certificate File Description ​

FileUsePermission recommendations
ca.pemCA certificates must be trusted by both the client and the server.644
ca-key.pemCA private key, keep it secure.600
server-cert.pemServer certificate644
server-key.pemserver private key600
client-cert.pemClient certificate (mutual authentication)644
client-key.pemClient private key (mutual authentication)600

4. Golang High-Performance TCP/TLS Receiver ​

4.1 Introduce a high-performance parsing library ​

The standard library has limited syslog parsing capabilities; using a specialized library is recommended:

bash
go get github.com/influxdata/go-syslog/v3
go get github.com/influxdata/go-syslog/v3/rfc5424
go get github.com/influxdata/go-syslog/v3/rfc3164

4.2 Complete Production-Grade TLS Receiver ​

go
package main

import (
    "crypto/tls"
    "crypto/x509"
    "encoding/json"
    "flag"
    "fmt"
    "io"
    "log"
    "net"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/influxdata/go-syslog/v3"
    "github.com/influxdata/go-syslog/v3/rfc3164"
    "github.com/influxdata/go-syslog/v3/rfc5424"
)

// Config configuration structure
type Config struct {
    ListenAddr   string
    CertFile     string
    KeyFile      string
    CAFile       string
    EnableTLS    bool
    RequireClientCert bool
    OutputFormat string   // json, raw
    BufferSize   int
}

// LogEntry standardized log entry (for output)
type LogEntry struct {
    Timestamp   string                 `json:"timestamp"`
    Hostname    string                 `json:"hostname"`
    Facility    int                    `json:"facility"`
    Severity    int                    `json:"severity"`
    SeverityStr string                 `json:"severity_str"`
    Tag         string                 `json:"tag,omitempty"`
    Message     string                 `json:"message"`
    Raw         string                 `json:"raw,omitempty"`
    Source      string                 `json:"source"`
    Extra       map[string]interface{} `json:"extra,omitempty"`
}

// Collector Syslog Collector
type Collector struct {
    config     Config
    parser     syslog.Machine
    outputChan chan LogEntry
    stopChan   chan struct{}
}

// NewCollector creates a collector
func NewCollector(config Config) *Collector {
    return &Collector{
        config:     config,
        parser:     syslog.NewParser(rfc3164.NewParser()), // Automatically detect RFC3164/5424
        outputChan: make(chan LogEntry, config.BufferSize),
        stopChan:   make(chan struct{}),
    }
}

// getTLSConfig gets the TLS configuration
func (c *Collector) getTLSConfig() (*tls.Config, error) {
    cert, err := tls.LoadX509KeyPair(c.config.CertFile, c.config.KeyFile)
    if err != nil {
        return nil, fmt.Errorf("load cert failed: %w", err)
    }

    tlsConfig := &tls.Config{
        Certificates: []tls.Certificate{cert},
        MinVersion:   tls.VersionTLS12,
    }

    if c.config.RequireClientCert {
        tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
        caCert, err := os.ReadFile(c.config.CAFile)
        if err != nil {
            return nil, fmt.Errorf("load CA failed: %w", err)
        }
        caPool := x509.NewCertPool()
        if !caPool.AppendCertsFromPEM(caCert) {
            return nil, fmt.Errorf("invalid CA cert")
        }
        tlsConfig.ClientCAs = caPool
    }

    return tlsConfig, nil
}

// parseMessage parses syslog messages
func (c *Collector) parseMessage(raw string, source string) *LogEntry {
    msg, err := c.parser.Parse([]byte(raw))
    if err != nil {
        // Parsing failed, returning original content
        return &LogEntry{
            Timestamp:   time.Now().UTC().Format(time.RFC3339),
            Message:     raw,
            Raw:         raw,
            Source:      source,
            SeverityStr: "unknown",
        }
    }

    entry := &LogEntry{
        Timestamp:   msg.GetTimestamp().Format(time.RFC3339),
        Hostname:    msg.GetHostname(),
        Message:     msg.GetMessage(),
        Raw:         raw,
        Source:      source,
        Extra:       make(map[string]interface{}),
    }

    // Extract Facility and Severity
    if p, ok := msg.GetPriority(); ok {
        entry.Facility = int(p.Facility)
        entry.Severity = int(p.Severity)
        entry.SeverityStr = severityToString(p.Severity)
    }

    // Extract TAG
    if tag, ok := msg.GetTag(); ok {
        entry.Tag = tag
    }

    return entry
}

// severityToString Priority value to string
func severityToString(s uint8) string {
    m := map[uint8]string{
        0: "emerg", 1: "alert", 2: "crit", 3: "err",
        4: "warning", 5: "notice", 6: "info", 7: "debug",
    }
    if str, ok := m[s]; ok {
        return str
    }
    return "unknown"
}

// handleConnection handles a single connection
func (c *Collector) handleConnection(conn net.Conn) {
    defer conn.Close()

    buf := make([]byte, 8192)
    for {
        select {
        case <-c.stopChan:
            return
        default:
        }

        n, err := conn.Read(buf)
        if err != nil {
            if err != io.EOF {
                log.Printf("Read error: %v", err)
            }
            return
        }

        // Process potentially multiple messages (separated by newlines)
        data := buf[:n]
        start := 0
        for i, b := range data {
            if b == '\n' {
                if i > start {
                    raw := string(data[start:i])
                    entry := c.parseMessage(raw, conn.RemoteAddr().String())
                    c.outputChan <- *entry
                }
                start = i + 1
            }
        }
        // Process the last line (may not have a newline character)
        if start < len(data) {
            raw := string(data[start:])
            entry := c.parseMessage(raw, conn.RemoteAddr().String())
            c.outputChan <- *entry
        }
    }
}

// outputProcessor Output Processor (outputs JSON format to stdout)
func (c *Collector) outputProcessor() {
    encoder := json.NewEncoder(os.Stdout)
    for {
        select {
        case <-c.stopChan:
            return
        case entry := <-c.outputChan:
            if c.config.OutputFormat == "json" {
                if err := encoder.Encode(entry); err != nil {
                    log.Printf("JSON encode error: %v", err)
                }
            } else {
                // Original format
                fmt.Printf("[%s] %s: %s\n", entry.Timestamp, entry.Hostname, entry.Message)
            }
        }
    }
}

// Run start collector
func (c *Collector) Run() error {
    // Start output processor
    go c.outputProcessor()

    var ln net.Listener
    var err error

    if c.config.EnableTLS {
        tlsConfig, err := c.getTLSConfig()
        if err != nil {
            return fmt.Errorf("TLS config failed: %w", err)
        }
        ln, err = tls.Listen("tcp", c.config.ListenAddr, tlsConfig)
        log.Printf("🔒 TLS listener started on %s", c.config.ListenAddr)
    } else {
        ln, err = net.Listen("tcp", c.config.ListenAddr)
        log.Printf("📡 Plain TCP listener started on %s", c.config.ListenAddr)
    }
    if err != nil {
        return fmt.Errorf("listen failed: %w", err)
    }
    defer ln.Close()

    // Accept connection
    for {
        select {
        case <-c.stopChan:
            return nil
        default:
        }

        conn, err := ln.Accept()
        if err != nil {
            log.Printf("Accept error: %v", err)
            continue
        }
        go c.handleConnection(conn)
    }
}

// Stop Stop collector
func (c *Collector) Stop() {
    close(c.stopChan)
}

func main() {
    // Command-line arguments
    addr := flag.String("addr", ":5514", "Listen address")
    tlsEnabled := flag.Bool("tls", false, "Enable TLS")
    certFile := flag.String("cert", "certs/server-cert.pem", "TLS certificate file")
    keyFile := flag.String("key", "certs/server-key.pem", "TLS key file")
    caFile := flag.String("ca", "certs/ca.pem", "CA certificate file")
    requireClientCert := flag.Bool("client-auth", false, "Require client certificate")
    outputFormat := flag.String("output", "json", "Output format (json/raw)")
    bufferSize := flag.Int("buffer", 10000, "Output channel buffer size")
    flag.Parse()

    config := Config{
        ListenAddr:        *addr,
        CertFile:          *certFile,
        KeyFile:           *keyFile,
        CAFile:            *caFile,
        EnableTLS:         *tlsEnabled,
        RequireClientCert: *requireClientCert,
        OutputFormat:      *outputFormat,
        BufferSize:        *bufferSize,
    }

    collector := NewCollector(config)

    // Graceful exit
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        <-sigChan
        log.Println("Shutting down...")
        collector.Stop()
    }()

    if err := collector.Run(); err != nil {
        log.Fatalf("Collector error: %v", err)
    }
}

4.3 Operation and Testing ​

bash
# Compile
go build -o syslog-collector main.go

# Run in plaintext mode
./syslog-collector -addr ":5514" -output json

# Run in TLS mode
./syslog-collector -tls -addr ":5514" -cert certs/server-cert.pem -key certs/server-key.pem

# Send test logs
echo "<34>Oct 11 22:14:15 mymachine su: 'su root' failed" | nc localhost 5514

5. Golang Sender Implementation ​

5.1 Standard library syslog example ​

go
package main

import (
    "log"
    "log/syslog"
)

func main() {
    // Connect to the local syslog service
    writer, err := syslog.Dial("tcp", "localhost:514", syslog.LOG_INFO|syslog.LOG_AUTH, "myapp")
    if err != nil {
        log.Fatal(err)
    }
    defer writer.Close()

    writer.Alert("Security alert: suspicious login detected")
    writer.Crit("Critical: PAM authentication failure")
    writer.Info("Normal login from 192.168.1.100")
}

5.2 Custom TLS Sender (Supports Mutual Authentication) ​

go
package main

import (
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "log"
    "net"
    "os"
    "time"
)

type TLSLogger struct {
    conn     net.Conn
    config   *tls.Config
    address  string
}

// NewTLSLogger creates a TLS log sender
func NewTLSLogger(address, certFile, keyFile, caFile string, requireCert bool) (*TLSLogger, error) {
    config := &tls.Config{
        MinVersion: tls.VersionTLS12,
    }

    // Load client certificate
    if certFile != "" && keyFile != "" {
        cert, err := tls.LoadX509KeyPair(certFile, keyFile)
        if err != nil {
            return nil, fmt.Errorf("load client cert: %w", err)
        }
        config.Certificates = []tls.Certificate{cert}
    }

    // Load CA (server-side certificate verification)
    if caFile != "" {
        caCert, err := os.ReadFile(caFile)
        if err != nil {
            return nil, fmt.Errorf("load CA: %w", err)
        }
        caPool := x509.NewCertPool()
        caPool.AppendCertsFromPEM(caCert)
        config.RootCAs = caPool
    }

    if requireCert {
        config.InsecureSkipVerify = false
    } else {
        config.InsecureSkipVerify = true // Test environment only
    }

    return &TLSLogger{
        config:  config,
        address: address,
    }, nil
}

// Connect establish connection
func (l *TLSLogger) Connect() error {
    conn, err := tls.Dial("tcp", l.address, l.config)
    if err != nil {
        return err
    }
    l.conn = conn
    return nil
}

// Send send syslog message
func (l *TLSLogger) Send(message string) error {
    if l.conn == nil {
        return fmt.Errorf("not connected")
    }
    _, err := l.conn.Write([]byte(message + "\n"))
    return err
}

// Close Close connection
func (l *TLSLogger) Close() error {
    if l.conn != nil {
        return l.conn.Close()
    }
    return nil
}

func main() {
    logger, err := NewTLSLogger("localhost:5140",
        "certs/client-cert.pem",
        "certs/client-key.pem",
        "certs/ca.pem",
        true)
    if err != nil {
        log.Fatal(err)
    }
    defer logger.Close()

    if err := logger.Connect(); err != nil {
        log.Fatal(err)
    }

    // Send test message
    messages := []string{
        "<34>Oct 11 22:14:15 mymachine su: 'su root' failed",
        "<38>Oct 11 22:15:00 mymachine sshd[1234]: Failed password for root",
        "<46>Oct 11 22:16:30 mymachine sudo: user ALL : command not allowed",
    }

    for _, msg := range messages {
        if err := logger.Send(msg); err != nil {
            log.Printf("Send error: %v", err)
        }
        time.Sleep(1 * time.Second)
    }
}

6. Integration with SIEM / Logging Platforms ​

6.1 JSON Output Format Example ​

The JSON format output by the collector:

json
{
  "timestamp": "2024-10-11T22:14:15Z",
  "hostname": "mymachine",
  "facility": 4,
  "severity": 2,
  "severity_str": "crit",
  "tag": "su",
  "message": "'su root' failed for lonvick on /dev/pts/8",
  "source": "192.168.1.100:54321",
  "extra": {}
}

6.2 Integrating with ELK Stack (Filebeat Configuration) ​

yaml
# filebeat.yml
filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /var/log/collector/*/*/*.log
  json.keys_under_root: true
  json.overwrite_keys: true

output.elasticsearch:
  hosts: ["localhost:9200"]
  index: "syslog-%{+yyyy.MM.dd}"

setup.template.name: "syslog"
setup.template.pattern: "syslog-*"

6.3 Integrating with Kafka (Golang Producer) ​

go
package main

import (
    "encoding/json"
    "github.com/IBM/sarama"
    "log"
)

type KafkaProducer struct {
    producer sarama.SyncProducer
    topic    string
}

func NewKafkaProducer(brokers []string, topic string) (*KafkaProducer, error) {
    config := sarama.NewConfig()
    config.Producer.RequiredAcks = sarama.WaitForAll
    config.Producer.Retry.Max = 5
    config.Producer.Return.Successes = true

    producer, err := sarama.NewSyncProducer(brokers, config)
    if err != nil {
        return nil, err
    }

    return &KafkaProducer{
        producer: producer,
        topic:    topic,
    }, nil
}

func (kp *KafkaProducer) Send(entry LogEntry) error {
    data, err := json.Marshal(entry)
    if err != nil {
        return err
    }

    msg := &sarama.ProducerMessage{
        Topic: kp.topic,
        Value: sarama.ByteEncoder(data),
    }

    _, _, err = kp.producer.SendMessage(msg)
    return err
}

func (kp *KafkaProducer) Close() error {
    return kp.producer.Close()
}

7. Performance Optimization and Production Recommendations ​

7.1 Performance Benchmark Comparison ​

Parsing methodQPSMemory usageApplicable scenarios
regular expression~5000lowSimple scenario
go-syslog library~20000MiddleProduction Recommendations
Custom state machine~50000HighExtreme performance requirements

7.2 Production Environment Checklist ​

  • TLS mutual authentication: Prevent unauthorized clients from sending fake logs

  • Current limiting protection Prevent malicious clients from exhausting service resources

  • Log rotation: Avoid disk full (use logrotate)

  • Monitoring alerts: Prometheus metrics exposure (connection count, parsing error count)

  • buffer queue: Use a ring buffer when the output is blocked

7.3 Rate Limiting Middleware Example ​

go
type RateLimiter struct {
    limiter *rate.Limiter
}

func NewRateLimiter(r rate.Limit, b int) *RateLimiter {
    return &RateLimiter{
        limiter: rate.NewLimiter(r, b),
    }
}

func (rl *RateLimiter) Middleware(next func(net.Conn)) func(net.Conn) {
    return func(conn net.Conn) {
        if !rl.limiter.Allow() {
            log.Printf("Rate limit exceeded, dropping connection from %s", conn.RemoteAddr())
            conn.Close()
            return
        }
        next(conn)
    }
}

8. Summary ​

This article completes the full evolution from basics to production:

  • ✅TLS Certificate Generation: Self-signed CA + server/client certificates

  • ✅Production-grade receiver: Supports TLS mutual authentication, high-performance parsing, and JSON output

  • ✅Sender implementation: Standard library + custom TLS sender

  • ✅SIEM integration: ELK / Kafka Integration Solution

  • ✅Performance optimization: Rate limiting, buffering, and monitoring recommendations

Next article preview: <Syslog Comparison: A Guide to Choosing Between syslog and journald Logs This will delve into the selection decisions for modern Linux dual-logging systems and provide an implementation of a unified log-reading layer in Golang.

Reference resources ​

Last updated on11/07/2026, 02:16 PM
@fishfinal | @fishfinals | [email protected]