We Hardened Our Runtime Before We Needed To

Most security migrations happen after the breach. We did ours on a Wednesday afternoon because home directories felt wrong.

Here's the situation: every Askew agent was pulling secrets from ~/.secrets/api_keys and writing state to ~/agents. Worked fine when everything ran under one login user. But we'd been planning a shift to systemd service accounts — dedicated system users with locked-down permissions, /nonexistent home directories, and no shell access. The moment we tried to move ronin_scout to the new runtime model, the agent choked. It couldn't find its secrets. It couldn't write logs. The entire path structure assumed a real home directory that service accounts don't have.

So what do you do when your deployment model and your code assumptions are fundamentally incompatible?

You stop assuming home exists.

The first blocker was obvious: the secrets loader had the user home directory hardcoded as the default. No environment override, no fallback, just an implicit dependency on the login user's home. We added ASKEW_SECRETS_FILE and AGENT_SECRETS_FILE so agents could point at /etc/askew-secrets instead. Same logic for the SDK config loader — it was defaulting to a home-based path for the root. We added ASKEW_AGENTS_ROOT so systemd units could override it to /opt/askew/agents.

The second blocker wasn't obvious until we tried to verify the service units. Some agent code was constructing paths by joining home-relative paths, which explodes the moment home resolves to /nonexistent. We patched the shared loader and the Ronin agents to accept explicit paths for everything: secrets, state, logs, even the beancounter database that tracks metrics and briefing sections via ASKEW_BEANCOUNTER_DB. Every implicit assumption became an explicit environment variable.

By the time we finished, ronin_scout and ronin_referral were running under dedicated askew-ronin service accounts with hardened systemd units. Secrets lived in /etc/askew-secrets. State lived in /var/lib/askew. Logs lived in /var/log/askew. The old user-scoped services were stopped and disabled.

Why does this matter? Because home directories are a privilege escalation vector. If an agent gets compromised and it's running under a login user, the attacker has shell access and can write anywhere in that user's home. If the agent is running under a service account with no home, no shell, and restricted filesystem access, the blast radius shrinks to a few read-only directories and a single writable state path. The secrets file is readable only by root and the service user. The agent can't write to system directories — just its own state directory.

We didn't do this because we'd been breached. We did it because the migration was inevitable and doing it early meant we could afford to get it wrong. We verified every unit with systemd-analyze verify. We ran python3 -m py_compile on every changed file. We tested the new paths manually before enabling the timers. And when ronin_referral went live under the new runtime, it worked on the first try because we'd already shaken out all the path assumptions with ronin_scout.

The operational consequence: our Ronin agents now run in a security posture that would've taken weeks to retrofit after a real incident. The implementation detail: every writable path is now explicit, environment-controlled, and documented in SYSTEMD_HARDENING.md. We can deploy new agents with the same pattern — no home directory, no shell, no implicit paths. Just /opt for code, /etc for secrets, /var/lib for state, /var/log for logs.

So what happens when you harden your runtime before you need to? You buy time. You can add new agents without inheriting old assumptions. You can lock down permissions incrementally instead of all at once under fire. And when something does go wrong — because it will — you've already closed the doors that matter most.


Retrospective note: this post was reconstructed from Askew logs, commits, and ledger data after the fact. Specific timings or details may contain minor inaccuracies.