Webhook Security: Beyond 'Just Verify the Signature'

Webhooks are deceptively simple: someone sends you HTTP requests, you process them. What could go wrong? Everything. Webhooks are inbound attack surface, and most implementations have gaps you could drive a truck through. The Obvious One: Signature Verification Most webhook providers sign their payloads. Stripe uses HMAC-SHA256. GitHub uses HMAC-SHA1 or SHA256. Slack uses its own signing scheme. You’ve probably implemented this: 1 2 3 4 5 6 7 8 9 10 11 import hmac import hashlib def verify_stripe_signature(payload: bytes, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode(), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(f"sha256={expected}", signature) Good. But this is table stakes. What else? ...

March 10, 2026 Â· 6 min Â· 1108 words Â· Rob Washington

Docker Secrets Management: From Development to Production

Secrets management in Docker is where most teams get bitten. Environment variables leak into logs, credentials end up in images, and “it works on my machine” becomes a security incident. Here’s how to handle secrets properly at every stage. The Problem with Environment Variables The most common approach—and the most dangerous: 1 2 3 4 5 6 # docker-compose.yml - DON'T DO THIS services: app: environment: - DATABASE_PASSWORD=super_secret_password - API_KEY=sk-live-1234567890 Why this fails: ...

March 7, 2026 Â· 5 min Â· 944 words Â· Rob Washington

Kill Your Bastion Hosts: SSM Session Manager is Better in Every Way

You’re still running a bastion host, aren’t you? That t3.micro sitting in a public subnet, port 22 open to… well, hopefully not 0.0.0.0/0, but let’s be honest — it’s probably close. Stop it. AWS Systems Manager Session Manager exists, and it’s better in every way. The Bastion Problem Bastion hosts have been the standard for decades. Jump box in a public subnet, SSH through it to reach private instances. Simple enough. ...

March 6, 2026 Â· 5 min Â· 992 words Â· Rob Washington

Environment Variable Management: Patterns That Scale

Environment variables seem trivial—just key-value pairs. Then you have 50 of them across 4 environments with secrets mixed in, and suddenly you’re in configuration hell. Here’s how to stay sane. The Hierarchy Configuration should flow from least to most specific: D e f a u l t s ( c o d e ) → C o n f i g f i l e s → E n v v a r s → C o m m a n d - l i n e f l a g s Each layer overrides the previous. Environment variables sit near the top—easy to change per environment without touching code. ...

March 5, 2026 Â· 5 min Â· 975 words Â· Rob Washington

Systemd Service Hardening: Security Beyond the Defaults

Most systemd service files are written for functionality, not security. The defaults give services more access than they need—full filesystem visibility, network capabilities, and the ability to spawn processes anywhere. A few directives can dramatically reduce the blast radius if that service gets compromised. The Security Baseline Start with this template for any service that doesn’t need special privileges: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 [Service] # Run as dedicated user User=myservice Group=myservice # Filesystem restrictions ProtectSystem=strict ProtectHome=true PrivateTmp=true ReadWritePaths=/var/lib/myservice # Network restrictions (if service doesn't need network) # PrivateNetwork=true # Capability restrictions NoNewPrivileges=true CapabilityBoundingSet= AmbientCapabilities= # System call filtering SystemCallArchitectures=native SystemCallFilter=@system-service SystemCallFilter=~@privileged @resources # Additional hardening ProtectKernelTunables=true ProtectKernelModules=true ProtectKernelLogs=true ProtectControlGroups=true RestrictRealtime=true RestrictSUIDSGID=true MemoryDenyWriteExecute=true LockPersonality=true Understanding Each Directive Filesystem Protection 1 ProtectSystem=strict Mounts /usr, /boot, and /efi read-only. The strict level also makes /etc read-only. Use full if the service needs to write to /etc. ...

March 5, 2026 Â· 4 min Â· 836 words Â· Rob Washington

Cloudflare Tunnels: Expose Local Services Without Port Forwarding

Port forwarding is a security liability. Opening ports means exposing your network, managing firewall rules, and hoping your ISP doesn’t change your IP. Cloudflare Tunnels solve this elegantly—your services connect outbound to Cloudflare, which handles incoming traffic. How Tunnels Work Traditional setup: I n t e r n e t → Y o u r P u b l i c I P : 4 4 3 → F i r e w a l l → N A T → L o c a l S e r v i c e With Cloudflare Tunnel: ...

March 5, 2026 Â· 5 min Â· 896 words Â· Rob Washington

Secrets Management: Keeping Credentials Out of Your Code

Hardcoded credentials in your repository are a security incident waiting to happen. One leaked .env file, one accidental commit, and your database is exposed to the internet. Let’s do secrets properly. The Basics What’s a Secret? Anything that grants access: Database passwords API keys OAuth tokens TLS certificates SSH keys Encryption keys Where Secrets Don’t Belong 1 2 3 # ❌ Never do this DATABASE_URL = "postgres://admin:supersecret123@db.prod.internal/myapp" AWS_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" Also bad: .env files committed to git Docker image layers CI/CD logs Chat messages Wikis or documentation Secret Storage Options Environment Variables Simple, but limited: ...

March 4, 2026 Â· 5 min Â· 1054 words Â· Rob Washington

SSL/TLS Certificate Management: Avoiding the 3 AM Expiry Crisis

Nothing ruins a morning like discovering your certificate expired overnight and customers are seeing security warnings. Let’s prevent that. Certificate Basics What You Actually Need A certificate contains: Your domain name(s) Your public key Certificate Authority’s signature Expiration date 1 2 3 4 5 # View certificate details openssl x509 -in cert.pem -text -noout # Check what's actually served openssl s_client -connect example.com:443 -servername example.com | openssl x509 -text -noout Certificate Types DV (Domain Validation): Proves you control the domain. Cheapest, fastest. ...

March 4, 2026 Â· 6 min Â· 1255 words Â· Rob Washington

Container Security: Practical Hardening for Production

Containers provide isolation, but they’re not magic security boundaries. A misconfigured container can expose your entire host. Let’s fix that. Don’t Run as Root The single biggest mistake: running containers as root. 1 2 3 4 5 6 7 8 9 10 11 12 # Bad: runs as root by default FROM node:20 COPY . /app CMD ["node", "server.js"] # Good: create and use non-root user FROM node:20 RUN groupadd -r appgroup && useradd -r -g appgroup appuser WORKDIR /app COPY --chown=appuser:appgroup . . USER appuser CMD ["node", "server.js"] Why it matters: if an attacker escapes the container while running as root, they’re root on the host. As a non-root user, they’re limited. ...

March 4, 2026 Â· 5 min Â· 1063 words Â· Rob Washington

Secrets Management in Production: Beyond Environment Variables

We’ve all done it. That first deployment where the database password lives in a .env file. The API key hardcoded “just for testing.” The SSH key committed to the repo because you were moving fast. Environment variables as secrets storage is the gateway drug of bad security practices. Let’s talk about what actually works. The Problem with Environment Variables Environment variables seem safe. They’re not in the code, right? But consider: ...

March 3, 2026 Â· 4 min Â· 796 words Â· Rob Washington