C.W.K.
Stream
Lesson 05 of 05 · published

The Self-Lockout Recovery — You Lock Yourself Out

~15 min · recovery, self-lockout, runbook

Level 0Greenhorn
0 XP0/53 lessons0/14 achievements
0/100 XP to next level100 XP to go0% complete

You will lock yourself out. It happens. New phone, fat fingers, or you just forgot the PIN after a long break. Plan the recovery now, not at 11pm on a Sunday when you're blacklisted from your own admin panel.

The three recovery paths

PathPre-requisitesUse when
1. Local-IP unlockPhysical access to a host in LOCAL_BYPASSYou're at home and your dev Mac is in the bypass list
2. Admin URL with secret tokenYou stashed a one-shot recovery token in 1PasswordYou're remote, can't get to a bypass IP
3. SSH + sqlite3SSH access to the host (still works even if PIN is broken)Last resort; nothing else available

Recovery path 3 — the always-available escape hatch

Even if the PIN is forgotten and the blacklist is full of your own IP, SSH plus a one-line SQL fixes it. SSH is itself authenticated (key-only, hopefully). Anyone with SSH can already do anything to the host — the security DB is just a file. Documenting this recovery path doesn't add a new attack surface; it just spares you future panic.

Recovery path 2 — the recovery token

For when you're remote and SSH is also broken (or you don't have a laptop). Generate a long random recovery token at PIN-set time, stash it in your password manager.

Code

Recovery via SSH + sqlite3·bash
# 1. SSH to the host
ssh me@my-mac

# 2. Open the security DB
sqlite3 ~/app/security.db

# 3. Wipe the blacklist and attempt counters for your IP
DELETE FROM security_blacklist WHERE ip = '100.64.0.7';
DELETE FROM security_attempts  WHERE ip = '100.64.0.7';

# 4. Optional: reset the PIN itself (use Python to bcrypt the new one)
# in another terminal:
python3 -c "import bcrypt; print(bcrypt.hashpw(b'1234', bcrypt.gensalt(12)).decode())"
# then in sqlite3:
UPDATE security_config SET pin_hash = 'NEW_HASH' WHERE id = 1;
.exit
Recovery token endpoint·python
RECOVERY_TOKEN = secrets.token_urlsafe(48)   # generated once, stored in 1Password
# stored as a hash in security_config

@app.post("/recover")
async def recover(request: Request, token: str = Form(...), new_pin: str = Form(...)):
    cfg = load_config()
    if not bcrypt.checkpw(token.encode(), cfg["recovery_hash"]):
        return Response("Forbidden", status_code=403)
    db.execute("UPDATE security_config SET pin_hash = ?", (hash_pin(new_pin),))
    db.execute("DELETE FROM security_blacklist")
    db.execute("DELETE FROM security_attempts")
    db.commit()
    return Response("PIN reset, blacklist cleared")

External links

Exercise

Generate a recovery token (python -c "import secrets; print(secrets.token_urlsafe(48))"). Store the bcrypt hash in security_config (add a recovery_hash column). Store the plaintext token in 1Password as 'Solo Auth Quest — Recovery Token'. Add the /recover endpoint. Test it once on a lockout you intentionally cause; you've now got the escape hatch.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.