Table of Contents
Stop Buying Security Tools and Start Fixing Your SSH Config
It was 2017. I was working at a fintech startup that was “disrupting” the lending space. We were moving fast, which is industry shorthand for “we didn’t have a staging environment that matched production.” I was tasked with spinning up a Redis instance for a new caching layer. I was tired, it was 11:00 PM, and I just wanted to see the latency numbers. I spun up an EC2 instance, installed Redis, and forgot to check the AWS Security Group. I also forgot that Redis, by default, binds to 0.0.0.0 if you aren’t careful. I figured I’d lock it down in the morning.
By 11:04 PM, the CPU usage on that node hit 100%. I ran top and saw a process named kworker/u:1. It looked like a kernel thread, but it was consuming 98% of a core. It wasn’t a kernel thread. It was a Monero miner. An automated scanner had found the open port 6379, executed a CONFIG SET dbfilename exploit, and dropped a cron job that pulled a binary from a compromised server in Russia. I didn’t just leak data; I gave a stranger a free pass into our VPC. I spent the next 48 hours rotating every single IAM key, database password, and SSH key in the company. That was my welcome to the reality of “cybersecurity tips”—the ones that actually matter aren’t found in a glossy vendor PDF.
The Marketing Lie of “Seamless Security”
Most cybersecurity tips you find online are written by content marketers who have never had to debug a production outage caused by a misconfigured WAF. They love words like “holistic” and “transformative.” In the real world, security is friction. If your security doesn’t cause a little bit of friction, it’s probably just theater. We’ve been told for a decade that we need to buy more “Next-Gen” tools. We don’t. We need to stop doing the stupid things first.
Documentation is often worse than useless. It’s misleading. Take the standard advice to “rotate passwords every 90 days.” NIST (the National Institute of Standards and Technology) literally told everyone to stop doing this years ago. Why? Because humans are predictable. If you force a rotation, Summer2023! becomes Fall2023!. It doesn’t stop attackers; it just annoys your engineers until they start storing passwords in plaintext notes.txt files. We need to move away from “best practices” that were written for the era of mainframe terminals and start looking at how modern infrastructure actually breaks.
Identity is the New Perimeter (And Yours is Leaking)
The days of the “hard shell, soft center” network are dead. If you still think your “internal” network is safe because it’s behind a VPN, you’re already compromised. You just don’t know it yet. The first of my cybersecurity tips is to treat every internal service as if it’s sitting on the public internet.
Stop using RSA keys for SSH. They are bulky and increasingly vulnerable. Use Ed25519. It’s faster, more secure, and the keys are shorter. If you’re still using id_rsa, run this right now:
ssh-keygen -t ed25519 -a 100 -C "[email protected]"
The -a 100 flag increases the number of KDF (Key Derivation Function) rounds, making it significantly harder for someone to brute-force the passphrase if they steal your private key file. But even better: stop managing raw SSH keys. Use a short-lived certificate authority like HashiCorp Vault or Teleport. If a developer’s laptop gets stolen at a coffee shop, you shouldn’t have to scramble to remove their public key from 500 authorized_keys files. The certificate should just expire.
Pro-tip: If you must use static keys, at least use the
ProxyJumpdirective in your~/.ssh/configto avoid exposing your internal nodes to the public internet. Never, ever put a private key on a jump box.
Your /etc/ssh/sshd_config is likely a mess of legacy defaults. Hardening it takes five minutes and prevents 90% of automated bot attacks. Here is a minimal, opinionated configuration that I’ve used across hundreds of production nodes:
# /etc/ssh/sshd_config
PermitRootLogin prohibit-password
MaxAuthTries 3
PubkeyAuthentication yes
PasswordAuthentication no
PermitEmptyPasswords no
KexAlgorithms [email protected]
Ciphers [email protected],[email protected]
MACs [email protected],[email protected]
X11Forwarding no
AllowAgentForwarding no
Notice I didn’t say PermitRootLogin no. I said prohibit-password. You want to be able to log in as root with a key for emergency recovery, but you never want a password to be an option. Also, limiting MaxAuthTries to 3 kills most brute-force scripts before they even get started.
Secrets Management: Environment Variables are a Trap
I see this in every “modern” stack: export DB_PASSWORD=supersecret. This is lazy, and it’s dangerous. Environment variables are not secret. They leak into logs. They are visible in /proc/self/environ. If an attacker gets a local file inclusion (LFI) vulnerability in your web app, the first thing they are going to do is read /proc/self/environ to get your AWS keys and database credentials.
Instead, mount secrets as files. If you’re in Kubernetes, use a CSI driver that pulls from AWS Secrets Manager or Vault and mounts them as a tmpfs volume. Your code should read the secret from a file path, not an environment variable. Here’s a quick example in Go of how you should be doing it:
package main
import (
"os"
"log"
"strings"
)
func getDatabaseDSN() string {
// Don't do this: dsn := os.Getenv("DB_DSN")
// Do this:
dat, err := os.ReadFile("/var/run/secrets/db_dsn")
if err != nil {
log.Fatalf("Failed to read secret: %v", err)
}
return strings.TrimSpace(string(dat))
}
func main() {
dsn := getDatabaseDSN()
// Connect to localhost:5432...
}
By using a file, you ensure the secret stays in memory and isn’t accidentally dumped when your app crashes and prints the environment to stdout. It also makes it much harder for a stray ps aux or a compromised monitoring agent to scrape your credentials.
The Container Security Myth: Alpine vs. Debian-slim
The internet loves Alpine Linux for Docker images because it’s small. “Small equals secure,” they say. They are wrong. Alpine uses musl instead of glibc. While musl is great, it handles DNS resolution and memory allocation differently. I’ve seen production outages where a Go binary compiled on a Mac behaved erratically in an Alpine container because of subtle cgo issues.
More importantly, Alpine’s package manager (apk) often lags behind on security patches for common libraries. I prefer debian-slim or, even better, Google’s distroless images. Distroless contains only your application and its runtime dependencies. No shell. No ls. No curl. If an attacker gets a shell injection into your app, they can’t do anything because there is no /bin/sh to execute.
- Distroless: Best for production. Zero bloat. Hard to debug (you have to use ephemeral containers or
kubectl debug). - Debian-slim: Best for general use. Familiar,
glibccompatible, and still relatively small. - Alpine: Good for simple static binaries, but a headache for Python, Node.js, or anything that needs native extensions.
If you’re still using node:latest or python:3.9, you are pulling in hundreds of unnecessary packages, each with its own CVEs. Pin your versions. Not just the app version, but the base image hash. Use sha256 instead of tags. Tags are mutable; hashes are not.
# Don't do this:
FROM node:18
# Do this:
FROM node:18.16.0-slim@sha256:e309e7571199976f6d6...
Network Egress: The Forgotten Firewall
Everyone focuses on ingress—keeping the bad guys out. Almost no one focuses on egress—keeping the bad guys from talking to their Command and Control (C2) servers. If your application server is compromised, the first thing it will try to do is reach out to the internet to download a payload or exfiltrate data. If you have a “deny-all” egress policy, that attack fails.
In Kubernetes, this means using NetworkPolicies. By default, K8s allows all traffic between all pods. This is a nightmare. You need to explicitly define what is allowed. Here is a policy that allows a pod to talk to DNS (kube-dns) and nothing else:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
This is one of those cybersecurity tips that will save your ass during the next Log4j-style event. The Log4j exploit relied on the server being able to make an outbound LDAP request to a malicious server. If your egress was locked down to only allow connections to your database and your internal API, the exploit would have been neutralized before it even started.
The “Internal Network” Fallacy and mTLS
I’ve worked at places where the “security” was just a VPN. Once you were on the VPN, you could curl any service without authentication. This is how lateral movement happens. If I compromise a low-stakes marketing microservice, I can then use it to hit the high-stakes billing service.
You need service-to-service authentication. Don’t rely on IP whitelisting. IPs are ephemeral in the cloud. Use Mutual TLS (mTLS). Tools like Istio or Linkerd make this “automatic,” but they also add a massive amount of YAML-hell and complexity. If you’re not at Google scale, you can implement mTLS with Nginx or even just simple API keys passed in headers—as long as they are rotated and managed.
The goal is “Zero Trust,” a buzzword I hate, but a concept I respect. It just means: “Verify everything, trust nothing.” Even if the request is coming from localhost, verify the identity of the caller.
Logging: If It’s Not Searchable, It’s Not Security
Most people log for debugging, not for security. Security logging requires a different mindset. You don’t just need to know that an error happened; you need to know who did it, where they came from, and what they were trying to achieve. This means your logs must be structured. If you’re still writing plaintext logs that look like User logged in at 12:00, you’re making your life impossible.
Use JSON logging. Always. It allows your SIEM (Security Information and Event Management) or even just a simple ELK stack to parse fields without brittle regex. Here is what a good security log looks like:
{
"timestamp": "2023-10-27T14:32:01.123Z",
"level": "INFO",
"event": "authentication_success",
"user_id": "user_88234",
"remote_ip": "192.168.1.45",
"request_id": "req-9912-abc",
"user_agent": "Mozilla/5.0...",
"mfa_used": true
}
With this structure, I can instantly run a query to see if user_88234 has logged in from multiple IPs in the last hour. If your logs are just strings, you’ll be stuck in “grep-hell” while the attacker is busy dumping your database.
Note to self: Ensure that logs are sent to a write-only destination. An attacker’s first move after gaining root is to run
rm -rf /var/log/*. If your logs are already off-box in a centralized system, they can’t hide their tracks.
The Real World: The “Sudo” Problem
Here is a “Gotcha” that only comes with experience: sudo is a massive attack surface. Most engineers have ALL=(ALL) NOPASSWD:ALL in their sudoers file because typing a password is annoying. This means any process running as that user can become root instantly. If you’re serious about cybersecurity tips, you need to restrict sudo to specific commands or, better yet, remove it entirely for application users.
Your application should run as a non-privileged user. In Docker, this is often ignored. The default user is root. If your app is compromised, the attacker is root. Fix it with a simple line in your Dockerfile:
# Create a system user
RUN addgroup --system appgroup && adduser --system appuser --ingroup appgroup
USER appuser
# Now the attacker is trapped in a low-privilege shell
Dependency Hell and the “Audit” Noise
If you run npm audit on a modern project, you’ll get 400 vulnerabilities. 398 of them are “ReDoS” (Regular Expression Denial of Service) in some dev-dependency that only runs on your laptop. This noise is dangerous because it leads to “alert fatigue.” You start ignoring the audit because it’s always red.
Stop using npm audit as your only source of truth. Use something like Snyk or Trivy, and configure it to only alert on “High” or “Critical” vulnerabilities that are actually reachable in your production code path. If a vulnerability is in a test runner, it’s a low priority. If it’s in your web framework’s header parser, it’s a P0.
Also, beware of curl | sh. We’ve all done it. curl -sSL https://install.some-tool.com | sudo bash. You are literally piping unverified code from the internet into a root shell. At least download the script, inspect it, and pin it to a specific version. Better yet, use the official package manager for your OS.
Hardening the Kernel (The SRE Way)
If you have access to the underlying host, there are a few sysctl tweaks that can break many common exploit chains. These aren’t “magic bullets,” but they increase the cost for an attacker. Edit /etc/sysctl.conf and add these:
# Disable source routing
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
# Ignore ICMP redirects (prevents MITM)
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
# Enable TCP SYN Cookies (prevents SYN flood)
net.ipv4.tcp_syncookies = 1
# Restrict dmesg to root (prevents info leaks)
kernel.dmesg_restrict = 1
# Disable kexec (prevents replacing the kernel)
kernel.kexec_load_disabled = 1
After adding these, run sysctl -p. These settings close off legacy networking features that are almost never used in modern cloud environments but are frequently exploited by attackers to redirect traffic or gain information about the system state.
The Security Theater of SOC2
I’ve been through four SOC2 audits. They are paperwork exercises. They ensure you have a policy that says you do code reviews, but they don’t actually check if your code reviews are effective. Don’t mistake compliance for security. Compliance is for your customers’ legal teams; security is for your engineers. You can be SOC2 compliant and still have a wide-open S3 bucket. In fact, most companies that get breached *are* compliant. Focus on the technical controls, not just the PDF in the Dropbox folder.
The Wrap-up
Security isn’t a product you buy; it’s a state of constant, disciplined reduction of your attack surface. Every line of code you don’t write, every port you don’t open, and every package you don’t install is one less thing an attacker can use against you. Stop looking for the “ultimate” tool and start fixing your SSH configs, locking down your egress traffic, and running your containers as non-root users. The most effective cybersecurity tips are the ones that make your infrastructure boring, predictable, and incredibly difficult to talk to.
If you haven’t rotated your personal SSH keys to Ed25519 yet, do it now. No excuses.
Related Articles
Explore more insights and best practices: