Comprehensive security documentation covering architecture, threat model, and best practices for the SpamAssassin MCP server.
- Security Philosophy
- Threat Model
- Security Architecture
- Access Control
- Input Validation
- Container Security
- Network Security
- Data Protection
- Monitoring and Incident Response
- Security Checklist
The SpamAssassin MCP server is built with a defensive-only security posture:
- ✅ Analysis and Detection: Email spam analysis, reputation checking, rule validation
- ✅ Educational Tools: Score explanations, rule testing in safe environments
- ✅ Configuration Management: Read-only configuration inspection, defensive rule updates
- ❌ NO Email Transmission: Cannot send, relay, or forward emails
- ❌ NO Malicious Content: Cannot generate spam, phishing, or malicious content
- ❌ NO Offensive Operations: No penetration testing or attack simulation capabilities
- Fail Secure: System fails to a secure state when errors occur
- Least Privilege: Minimal permissions and access rights
- Defense in Depth: Multiple layers of security controls
- Zero Trust: Verify all inputs and validate all operations
- Transparency: Full audit logging and monitoring
- Privacy by Design: Minimal data collection and processing
| Asset | Value | Threats |
|---|---|---|
| Email Content | High | Data leakage, unauthorized access, content manipulation |
| System Configuration | Medium | Unauthorized changes, privilege escalation |
| Service Availability | High | DoS attacks, resource exhaustion |
| Host System | Critical | Container escape, privilege escalation |
| Network Communications | Medium | Interception, man-in-the-middle attacks |
Threat: Specially crafted emails designed to exploit vulnerabilities Mitigations:
- Strict input validation and size limits (10MB max)
- Email format validation using Go's
mailpackage - Content sanitization before processing
- Timeout protection (60s scan limit)
- Process isolation in containers
Threat: Excessive requests or malformed API calls Mitigations:
- Rate limiting (60 requests/minute, burst 10)
- Request size validation
- Parameter type checking
- Structured error responses (no data leakage)
Threat: Breaking out of container to access host system Mitigations:
- Non-root user execution (
spamassassinuser) - Read-only root filesystem
- No new privileges (
--security-opt no-new-privileges) - Resource limits (CPU, memory)
- Capability dropping
Threat: Network-based attacks against the service Mitigations:
- Isolated Docker networks
- Minimal exposed ports (only 8080)
- Rate limiting at application and network levels
- Health check monitoring
Threat: Unauthorized modification of security settings Mitigations:
- Read-only configuration mounts
- Configuration validation on startup
- Immutable container deployment
- Audit logging of configuration access
┌─────────────────────────────────────────────┐
│ Load Balancer │
│ (Rate Limiting, DDoS Protection) │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ Network Layer │
│ (Firewall, Network Isolation, TLS) │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ Application Layer │
│ (Input Validation, Rate Limiting, Auth) │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ Container Layer │
│ (Non-root, Read-only, Resource Limits) │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ Host Layer │
│ (OS Hardening, Monitoring, Logging) │
└─────────────────────────────────────────────┘
- Network Boundary: Firewall, load balancer, network isolation
- Application Boundary: Input validation, rate limiting, authentication
- Container Boundary: Process isolation, resource limits, capability restrictions
- System Boundary: OS-level security, audit logging, monitoring
Currently the server operates in a trusted environment without authentication. For production deployment:
- API Key Authentication
type APIKeyAuth struct {
validKeys map[string]bool
}
func (a *APIKeyAuth) Authenticate(req *http.Request) bool {
apiKey := req.Header.Get("X-API-Key")
return a.validKeys[apiKey]
}- JWT Token Authentication
func validateJWT(tokenString string) (*jwt.Token, error) {
return jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method")
}
return []byte(secretKey), nil
})
}- mTLS (Mutual TLS)
# nginx configuration for mTLS
server {
ssl_client_certificate /etc/nginx/client-ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;
}Planned RBAC implementation:
| Role | Permissions | Use Case |
|---|---|---|
| Analyzer | scan_email, explain_score | Email analysis only |
| Administrator | All tools + config management | Full system access |
| Monitor | get_config, health checks | Monitoring and diagnostics |
| Developer | test_rules, scan_email | Rule development and testing |
func (h *Handler) validateEmailContent(content string) error {
// Size validation
if len(content) > int(h.security.MaxEmailSize) {
return fmt.Errorf("email size exceeds limit of %d bytes", h.security.MaxEmailSize)
}
// Content validation
if content == "" {
return fmt.Errorf("email content cannot be empty")
}
// Format validation using Go's mail package
if _, err := mail.ReadMessage(strings.NewReader(content)); err != nil {
return fmt.Errorf("invalid email format: %w", err)
}
// Header sanitization
content = sanitizeHeaders(content)
return nil
}
func sanitizeHeaders(content string) string {
// Remove potentially dangerous headers
dangerousHeaders := []string{
"X-Original-To:",
"Return-Path:",
"Envelope-To:",
}
lines := strings.Split(content, "\n")
var sanitized []string
for _, line := range lines {
safe := true
for _, header := range dangerousHeaders {
if strings.HasPrefix(strings.ToLower(line), strings.ToLower(header)) {
safe = false
break
}
}
if safe {
sanitized = append(sanitized, line)
}
}
return strings.Join(sanitized, "\n")
}var (
emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
ipRegex = regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}$`)
domainRegex = regexp.MustCompile(`^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
)
func validateEmailAddress(email string) error {
if !emailRegex.MatchString(email) {
return fmt.Errorf("invalid email address format")
}
return nil
}
func validateIPAddress(ip string) error {
if !ipRegex.MatchString(ip) {
return fmt.Errorf("invalid IP address format")
}
// Additional validation for private/reserved ranges
if net.ParseIP(ip) == nil {
return fmt.Errorf("invalid IP address")
}
return nil
}type RateLimiter struct {
limiter *rate.Limiter
clients map[string]*rate.Limiter
mutex sync.RWMutex
}
func NewRateLimiter(requestsPerMinute int, burstSize int) *RateLimiter {
return &RateLimiter{
limiter: rate.NewLimiter(
rate.Every(time.Minute/time.Duration(requestsPerMinute)),
burstSize,
),
clients: make(map[string]*rate.Limiter),
}
}
func (rl *RateLimiter) Allow(clientID string) bool {
rl.mutex.RLock()
clientLimiter, exists := rl.clients[clientID]
rl.mutex.RUnlock()
if !exists {
rl.mutex.Lock()
clientLimiter = rate.NewLimiter(rl.limiter.Limit(), rl.limiter.Burst())
rl.clients[clientID] = clientLimiter
rl.mutex.Unlock()
}
return clientLimiter.Allow()
}# Use specific version, not latest
FROM golang:1.21-alpine AS builder
# Create non-root user
RUN adduser -D -s /bin/sh appuser
# Build with security flags
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags='-w -s -extldflags "-static"' \
-a -installsuffix cgo \
-o mcp-server main.go
# Production stage
FROM ubuntu:22.04
# Install only necessary packages
RUN apt-get update && apt-get install -y --no-install-recommends \
spamassassin \
ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# Create application user with specific UID/GID
RUN groupadd -r -g 1000 spamassassin && \
useradd -r -u 1000 -g spamassassin -d /home/spamassassin -s /bin/bash spamassassin
# Set secure file permissions
COPY --from=builder --chown=spamassassin:spamassassin /app/mcp-server /usr/local/bin/mcp-server
RUN chmod 755 /usr/local/bin/mcp-server
# Switch to non-root user
USER spamassassin
# Security labels
LABEL security.policy="defensive-only"
LABEL security.risk="low"
LABEL security.compliance="CIS-Docker-Benchmark"# docker-compose.yml security configuration
services:
spamassassin-mcp:
security_opt:
- no-new-privileges:true
- apparmor:docker-default
- seccomp:default
read_only: true
tmpfs:
- /tmp:noexec,nosuid,size=100m
- /var/run:noexec,nosuid,size=50m
cap_drop:
- ALL
cap_add:
- CHOWN
- SETGID
- SETUID
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
pids: 100
reservations:
memory: 256M
cpus: '0.25'Regular security scanning with tools like:
# Vulnerability scanning
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
-v $(pwd):/src aquasec/trivy image spamassassin-mcp:latest
# Container security benchmark
docker run --rm --net host --pid host --userns host --cap-add audit_control \
-e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
-v /var/lib:/var/lib:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /usr/lib/systemd:/usr/lib/systemd:ro \
-v /etc:/etc:ro \
docker/docker-bench-securitynetworks:
spamassassin-internal:
driver: bridge
internal: true
ipam:
config:
- subnet: 172.20.0.0/24
spamassassin-external:
driver: bridge
ipam:
config:
- subnet: 172.21.0.0/24Production deployment should use TLS:
# nginx TLS configuration
server {
listen 443 ssl http2;
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
# Strong SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
}# iptables rules for production
iptables -A INPUT -p tcp --dport 8080 -s 10.0.0.0/8 -j ACCEPT
iptables -A INPUT -p tcp --dport 8080 -j DROP
iptables -A INPUT -p tcp --dport 22 -s 192.168.1.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j DROP
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -j DROP| Data Type | Classification | Retention | Protection |
|---|---|---|---|
| Email Content | Confidential | Processing only | In-memory only, immediate disposal |
| Scan Results | Internal | Session only | Structured logging without content |
| Configuration | Internal | Persistent | Encrypted at rest, access controls |
| Audit Logs | Internal | 90 days | Encrypted, tamper-evident |
| Performance Metrics | Internal | 30 days | Aggregated, anonymized |
type SecureEmailProcessor struct {
maxSize int64
timeout time.Duration
}
func (p *SecureEmailProcessor) ProcessEmail(content string) (*ScanResult, error) {
// Input validation
if err := p.validateInput(content); err != nil {
return nil, err
}
// Process in memory only
ctx, cancel := context.WithTimeout(context.Background(), p.timeout)
defer cancel()
result, err := p.scanWithTimeout(ctx, content)
// Explicitly clear sensitive data
content = ""
runtime.GC()
return result, err
}
func (p *SecureEmailProcessor) scanWithTimeout(ctx context.Context, content string) (*ScanResult, error) {
done := make(chan *ScanResult, 1)
errChan := make(chan error, 1)
go func() {
result, err := p.performScan(content)
if err != nil {
errChan <- err
return
}
done <- result
}()
select {
case result := <-done:
return result, nil
case err := <-errChan:
return nil, err
case <-ctx.Done():
return nil, fmt.Errorf("scan timeout exceeded")
}
}type SecureLogger struct {
logger *logrus.Logger
}
func (l *SecureLogger) LogScanRequest(userID string, emailSize int, scanType string) {
l.logger.WithFields(logrus.Fields{
"user_id": hashUserID(userID), // Hash for privacy
"email_size": emailSize,
"scan_type": scanType,
"timestamp": time.Now().UTC(),
"session_id": generateSessionID(),
}).Info("Email scan requested")
}
func hashUserID(userID string) string {
h := sha256.Sum256([]byte(userID + secretSalt))
return hex.EncodeToString(h[:8]) // First 8 bytes for logging
}For sensitive configuration data:
# Using Docker secrets
secrets:
api_keys:
external: true
tls_cert:
external: true
services:
spamassassin-mcp:
secrets:
- source: api_keys
target: /run/secrets/api_keys
mode: 0400type SecurityMonitor struct {
alertThresholds map[string]float64
alertManager AlertManager
}
func (m *SecurityMonitor) MonitorRequest(req *Request) {
// Monitor for suspicious patterns
if m.detectAnomalousRequest(req) {
m.alertManager.SendAlert(SecurityAlert{
Type: "anomalous_request",
Severity: "medium",
Details: req.Summary(),
Timestamp: time.Now(),
})
}
// Monitor rate limiting violations
if req.RateLimited {
m.alertManager.SendAlert(SecurityAlert{
Type: "rate_limit_violation",
Severity: "low",
ClientID: req.ClientID,
Timestamp: time.Now(),
})
}
}var (
securityRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "security_requests_total",
Help: "Total number of security-related requests",
},
[]string{"type", "status"},
)
suspiciousActivityTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "suspicious_activity_total",
Help: "Total number of suspicious activities detected",
},
[]string{"type", "severity"},
)
)- Automated monitoring alerts
- Log analysis and correlation
- User-reported issues
- External threat intelligence
#!/bin/bash
# Emergency response script
INCIDENT_TYPE=$1
SEVERITY=$2
case $SEVERITY in
"critical")
# Immediate isolation
docker-compose down
iptables -A INPUT -j DROP
;;
"high")
# Rate limiting increase
# Enable additional monitoring
;;
"medium")
# Enhanced logging
# Notify security team
;;
esac
# Log incident
echo "$(date): Incident $INCIDENT_TYPE with severity $SEVERITY" >> /var/log/security-incidents.log- Service restoration procedures
- Security patch application
- Configuration updates
- Lessons learned documentation
type AuditLogger struct {
logger *logrus.Logger
}
func (a *AuditLogger) LogSecurityEvent(event SecurityEvent) {
a.logger.WithFields(logrus.Fields{
"event_type": event.Type,
"user_id": hashString(event.UserID),
"source_ip": hashString(event.SourceIP),
"action": event.Action,
"resource": event.Resource,
"result": event.Result,
"timestamp": event.Timestamp.UTC(),
"session_id": event.SessionID,
"risk_score": event.RiskScore,
}).Warn("Security event detected")
}
type SecurityEvent struct {
Type string `json:"type"`
UserID string `json:"user_id"`
SourceIP string `json:"source_ip"`
Action string `json:"action"`
Resource string `json:"resource"`
Result string `json:"result"`
Timestamp time.Time `json:"timestamp"`
SessionID string `json:"session_id"`
RiskScore float64 `json:"risk_score"`
}- Security code review completed
- Vulnerability scanning passed
- Container security benchmarks met
- Network segmentation configured
- Firewall rules implemented
- TLS certificates installed and valid
- Secrets management configured
- Monitoring and alerting enabled
- Backup and recovery procedures tested
- Incident response plan reviewed
- Services running as non-root users
- Resource limits enforced
- Rate limiting active
- Input validation functioning
- Audit logging operational
- Security monitoring alerts working
- Regular security updates scheduled
- Access controls verified
- Security patches applied within 24 hours
- Vulnerability scans run weekly
- Log analysis performed daily
- Security metrics reviewed monthly
- Incident response tested quarterly
- Security training completed annually
- Third-party security audit completed annually
#!/bin/bash
# Security validation script
echo "=== SpamAssassin MCP Security Validation ==="
# Check container security
echo "Checking container security..."
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image spamassassin-mcp:latest
# Verify non-root execution
USER_CHECK=$(docker-compose exec spamassassin-mcp whoami)
if [ "$USER_CHECK" != "spamassassin" ]; then
echo "ERROR: Container not running as spamassassin user"
exit 1
fi
# Check file permissions
PERM_CHECK=$(docker-compose exec spamassassin-mcp stat -c "%a" /usr/local/bin/mcp-server)
if [ "$PERM_CHECK" != "755" ]; then
echo "ERROR: Incorrect binary permissions"
exit 1
fi
# Verify rate limiting
echo "Testing rate limiting..."
for i in {1..70}; do
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/health
done | grep "429" > /dev/null
if [ $? -eq 0 ]; then
echo "✓ Rate limiting working"
else
echo "⚠ Rate limiting may not be working"
fi
# Check TLS configuration
echo "Checking TLS configuration..."
if command -v testssl.sh >/dev/null 2>&1; then
testssl.sh --quiet --color 0 https://localhost:8080
fi
echo "✓ Security validation completed"- Minimal data collection (no PII storage)
- Data processing transparency
- Right to erasure (automatic data disposal)
- Data protection by design and default
- Access controls and authentication
- System availability and performance monitoring
- Security monitoring and incident response
- Change management procedures
- Information security management system
- Risk assessment and treatment
- Security controls implementation
- Continuous improvement process
For security issues or vulnerabilities:
- Email: security@company.com
- PGP Key: Available at security@company.com
- Response Time: 24 hours for critical issues
For non-critical security questions, please refer to this documentation or create an issue in the project repository.