Baobaobaolin.com
date
entry
023
topic
tooling
rev

Your guardrail has never actually fired

The identity gate at the top of a deploy script only executes when the identity is wrong — and in normal operation, nobody walks that path. Yesterday I ran it with the wrong identity on purpose and found two problems inside two minutes.

In the earlier piece on AWS accounts I recommended a gate at the top of any script that mutates something:

EXPECTED_ACCOUNT=111111111111
ACTUAL=$(aws sts get-caller-identity --query Account --output text)
if [ "$ACTUAL" != "$EXPECTED_ACCOUNT" ]; then
  echo "WRONG ACCOUNT: $ACTUAL" >&2
  exit 1
fi

That code has run many times in my deploy script and passed every time. It looked healthy — but passing is the only behaviour of it that had ever been exercised.

Problem one: it lets root through

An account id identifies an account, not an identity. The root user, every IAM user and every role all belong to the same account.

$ aws sts get-caller-identity
{
  "Account": "060795942439",
  "Arn":     "arn:aws:iam::060795942439:root"
}

Account matches exactly, so the gate opens. The check meant to ensure "not running in the wrong place" has no opinion whatsoever about running with a wildly over-privileged identity.

To gate on identity rather than account, compare the ARN:

EXPECTED_ARN="arn:aws:iam::060795942439:user/deploy"
ARN=$(aws sts get-caller-identity --query Arn --output text) || exit 1
[ "$ARN" = "$EXPECTED_ARN" ] || { echo "wrong identity: $ARN" >&2; exit 1; }

One query field of difference. Comparing the account, an accidental root deploy succeeds silently; comparing the ARN, it stops.

Problem two: the error message crashes

The second one is sillier. My rewritten message read:

echo "       expected $EXPECTED_ARN(profile: $AWS_PROFILE). Aborting." >&2

What actually came out:

ERROR: current identity is arn:aws:iam::060795942439:root
./scripts/deploy.sh: line 29: EXPECTED_ARN?: unbound variable

A full-width parenthesis directly after a variable name is not treated by bash as terminating the name, so it went looking for a variable called EXPECTED_ARN(. There isn't one, and the script runs with set -u, so the whole line became an unbound-variable error.

The fix is braces: ${EXPECTED_ARN}(profile: ${AWS_PROFILE}). ASCII parentheses do not have this problem, which is why only the Chinese message hit it.

No harm resulted this time — the gate still blocked the run, it just exited via a bash error instead of my explanation. But that is precisely the point: that line only executes when the gate needs to stop someone, and until then it had never executed at all.

Code that only runs on failure can be broken for years with no symptom — because its symptom appears exactly when you need it most.

Negative testing is doing it wrong on purpose, once

Finding both took no tooling. It took running the script with the wrong identity:

AWS_PROFILE=default ./scripts/deploy.sh

There are two acceptance criteria, and you need both:

  1. It stopped — nothing was written to production
  2. It stopped with the sentence you wrote — not a bash error, not a stack trace, not silence

The second is the one people skip. Seeing "it blocked" feels like success, but if it blocks by crashing, the next person to hit it receives something unreadable instead of "you are using the wrong identity, here is the right one".

What else only runs on failure

There is more of this than you would think. The ones I now trigger deliberately:

  • Error-handling branches, especially the "this cannot happen" ones
  • Alert delivery — the rule is configured, but does that webhook still work? Is the recipient still employed?
  • Rollback paths — the release procedure gets rehearsed constantly, the un-release usually never
  • 404 and error pagesis the status code right
  • Backup restores

That last one deserves its own sentence: a backup you have never restored is not a backup, it is a file taking up space. The backup job succeeding daily, writing files daily, monitoring all green — none of that is evidence that the data can be recovered. The only evidence is having recovered it once.

On delivery projects I put the restore drill in the deliverables rather than in the operations recommendations. The reasoning is practical: as long as it is not a deliverable, it stays scheduled for next quarter forever.

It costs so little there is no excuse

This negative test took about two minutes: change one environment variable, run once, read four lines of output. It surfaced two problems, one of which made the entire gate ornamental.

Making it a habit needs no framework — right after writing any code whose job is to stop something bad, make the bad thing happen once. That is the closest that code will ever be to being verified; afterwards you will forget you wrote it.

If you remember one thing

The check needs checking too. A check that has never reported a problem has two possible explanations: everything is fine, or it is broken. Those look identical on a dashboard, and the only way to tell them apart is to deliberately cause the problem it is supposed to catch.

Revision history

  1. First published