Why I Keep a Separate Password for Every Environment

I keep a different password for the same service account on every environment it exists in, even on a project where I'm the only person who'll ever type any of them in. That habit paid for itself directly tonight, in a way that would have been a lot more confusing to untangle if I'd taken the obvious shortcut instead.

The setup

A publishing script needs to authenticate against Drupal's JSON:API as a scoped service account, both locally and in production, and each environment's copy of that account got its own separately generated password — one in a local config file, one in a production one. The account name is identical everywhere. Only the secret differs.

Where that almost got confusing anyway

Midway through some unrelated debugging, I pulled a fresh copy of the production database down into the local environment — which is routine, and which means local's copy of every table, including the users table, briefly becomes byte-identical to production's. That's the whole point of doing it. But it also means the service account's password hash on the local database just became production's password hash, silently, as an ordinary side effect of a routine sync.

The local publish script kept using the local config file's password, unchanged, because nothing told it to do otherwise. And that password no longer matched what was actually stored in the local database it was now talking to. The next request failed with a bare 403 and no explanation — Drupal's basic auth doesn't loudly reject bad credentials, it just quietly falls back to treating the request as anonymous, and anonymous doesn't have permission to create content. So the actual failure — a stale local password — surfaced two layers removed from itself, as a permissions error that had nothing to do with permissions.

Why the separate passwords made this findable

The fix was to actually test authentication directly rather than guess: hand the service the local password and see what comes back, then hand it the production password and see what comes back. One returned nothing. The other returned the account. That's a clean, unambiguous signal precisely because the two passwords were different values — if they'd been the same string in both environments, the second test would have told me nothing, because it would have "worked" for the wrong reason. The account still authenticating locally would have looked like confirmation that everything was fine, when the actual fact worth knowing — that local's database had just become a full copy of production's, not a stale local sandbox — would have stayed invisible.

The better fix I still haven't wired in

The real, more thorough answer to all of this isn't noticing the mismatch cleverly — it's not letting the mismatch happen in the first place. Drush ships exactly the command for it: drush sql:sanitize, meant to run right after a production database lands somewhere that isn't production. It scrambles every user's password to something random and unrecoverable, and it rewrites email addresses to a safe, non-deliverable pattern so a development environment can never accidentally send real mail to a real person. Real user passwords, plenty of which are reused elsewhere across the actual internet whether anyone wants to admit that or not, have no reason to exist unencrypted-in-practice on a laptop's local database at all — sanitizing on import isn't really about my one service account, it's about every other account in that table too.

My own fetch script doesn't call it yet. It's a straight drush sql-dump on production piped directly into the local database, no sanitize step in between — which is exactly the gap that let a production password hash travel somewhere it didn't need to go. Adding the call is one line. The part worth being honest about is that a scoped service account with two different passwords caught the symptom of that gap tonight; it didn't close it.

The part I actually value about this habit

Using one shared password across environments feels like it removes a source of friction, and on a personal project, it's tempting precisely because there's no real access-control reason not to — I'm the only one with either password anyway. But the friction it removes is exactly the friction that would have told me something meaningful had changed underneath me. A password that stops working isn't just an inconvenience to fix. On a project this size, it's very often the only signal you get that an environment isn't in the state you assumed it was.

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Please share this article on your favorite website or platform.