"One uppercase, one number, one symbol" — the rule that produced P@ssw0rd1 a billion times.
Key Kettle takes a password policy expressed as JSON and returns a scored, graded, prioritized report: what's wrong, how to fix it, why it matters, and which standard says so. It's the tool you run before a client meeting so you can hand over "here are your 6 failing controls, worst first" instead of a vague "your password rules feel old."
It is a defensive, offline advisory tool. It reads a policy description — it never touches a directory, an authentication system, or a real password. The only network surface is an optional local report UI, and that is gated (see Safety & authorization).
Most password policies in the wild still carry controls that current guidance actively discourages: short maximum lengths, forced periodic rotation, rigid composition rules, blocked paste / password managers, and fast general-purpose hashes for storage. Key Kettle turns those requirements into a standards-aligned improvement plan a compliance consultant, MSP, or internal security team can act on.
Every finding cites its source:
- NIST SP 800-63B — length over composition, no forced rotation, blocklist screening, rate limiting, slow hashing.
- OWASP Authentication / Password Storage Cheat Sheets — implementation specifics.
- CISA Multifactor Authentication — MFA emphasis.
- Verizon DBIR — why credential abuse stays a practical attacker path.
This is advisory tooling, not legal advice. A regulator or contract may demand controls stricter than baseline guidance; treat the report as a starting point.
policy JSON ──▶ PasswordPolicy.from_dict ──▶ analyze_policy ──▶ PolicyReport
(validate + coerce + (one check per (score, grade,
fill weak defaults) control) summary, recs)
│ │
├─ policy_key_warnings ──▶ stderr notes ├─ rich table/panel (humans)
│ └─ pure JSON (machines)
1. Parse & validate (policy.py). The input is treated as hostile. Fields
are coerced to their real types (booleans accept true/"yes"/1, rotation
accepts 0/""/null as "disabled"), ranges are checked (min_length within
1..4096, max_length ≥ min_length, no negative or absurd values), and bad
input raises a PolicyError with the offending field named. Missing fields
fall back to the weak sample on purpose — an audit tool should assume the
worst about controls it wasn't told about, not invent a flattering default.
Unknown/typo'd and omitted fields are surfaced as non-fatal notes on stderr.
2. Analyze (analyzer.py). Eleven controls are evaluated, each producing
exactly one recommendation — a pass or an issue — so the output is a complete
checklist, not just a pile of complaints:
| Control | Passes when | Weight if failing |
|---|---|---|
| Minimum length | ≥ 15 (or ≥ 8 when MFA is required) | high |
| Maximum length | ≥ 64 | medium |
| Composition rules | none required | medium (advisory) |
| Periodic rotation | none | medium/low (advisory) |
| Compromised-password screening | enabled | high |
| Multi-factor authentication | required | high |
| Password managers & paste | both allowed | medium |
| Rate limiting | 1–10 failed attempts | high |
| Password storage | salted hashing | critical |
| Password hashing algorithm | Argon2id / bcrypt / scrypt / PBKDF2 | high (advisory) |
| Character allowance | spaces + Unicode | low (advisory) |
3. Score & grade. Start at 100, subtract each non-passing control's severity
weight (critical 28, high 18, medium 10, low 4), floor at 0. Grades: A ≥ 90,
B ≥ 80, C ≥ 70, D ≥ 60, else F. Recommendations are sorted worst-first
in both the table and the JSON.
4. Render. Humans get a rich summary panel + colour-coded table (severity is
critical/high → red, medium → yellow, low → cyan, pass → green).
Machines get pure JSON. The two never mix.
PasswordPolicy (frozen dataclass, no database) captures: min_length,
max_length, the four require_* composition flags, rotation_days,
breach_screening, mfa_required, paste_allowed,
password_manager_allowed, lockout_threshold, stores_hashed,
hashing_algorithm, allows_spaces, allows_unicode. Run key-kettle sample-policy to see the full shape.
cd key-kettle
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
pip install -e . # provides the `key-kettle` command
cp .env.example .env # optional: sets default serve host/portNo editable install? Every command also works as
PYTHONPATH=src python -m key_kettle.cli ....
Global: --plain / --no-color disables colour, emoji, and box-drawing (also
auto-honoured when NO_COLOR is set or output isn't a TTY). --version prints
the version. JSON output is always pure regardless of these.
key-kettle sample-policy # a deliberately dated weak baseline
key-kettle sample-policy --strong # a modern, NIST-aligned baseline
key-kettle sample-policy > policy.jsonPrints policy JSON to stdout (the branded header, if any, goes to stderr, so the JSON stays pipeable).
key-kettle analyze --policy policy.json # human report
key-kettle analyze --policy - < policy.json # read from stdin
key-kettle sample-policy | key-kettle analyze --policy - # pipe straight in
key-kettle analyze --policy policy.json --json | jq # machine-readable
key-kettle analyze --policy policy.json --fail-under 80 # stricter CI gateExit codes: 0 = score meets the threshold; 1 = below threshold or an
input error (missing file, bad JSON, invalid field). Handy as a CI guard.
Sample human output (strong policy):
┌───────────────────────── Audit result ─────────────────────────┐
│ 100/100 grade A │
│ 0 failing controls, 0 warnings. │
│ 0 failing · 0 advisory · 11 passing · 11 controls reviewed │
└─────────────────────────────────────────────────────────────────┘
Sev Control Finding Recommended fix
─────────────────────────────────────────────────────────────────────────────
✓ pass Multi-factor authentication MFA is required. Keep MFA required…
✓ pass Rate limiting Failed attempts… Keep tight rate…
…
And for a weak policy, the same view leads with the worst findings:
X high Compromised-password screening No blocklist… Compare new passwords…
X high Minimum length Minimum length 8… Set minimum length to 15…
X high Multi-factor authentication MFA is not req… Require MFA…
key-kettle serve # http://127.0.0.1:5065
key-kettle serve --host 127.0.0.1 --port 5065Opens a Flask page where you paste policy JSON and get the rendered report. Defaults to loopback. Binding anywhere else requires an explicit acknowledgement (see below).
Key Kettle is for auditing policies you own or are authorized to assess. It
performs no scanning and sends no email. The one surface worth care is serve:
- Binds to
127.0.0.1by default. The report UI has no authentication and evaluates policy JSON pasted by whoever can reach it. - Non-loopback binds are gated.
serve --host 0.0.0.0(or any LAN IP / hostname) is refused unless you pass--allow-nonlocal, an explicit "I own this network and accept the exposure" acknowledgement. Refusal exit code is2. --debugon a non-loopback host is always refused — the Werkzeug interactive debugger allows remote code execution.- Input is treated as hostile. File reads are capped (1 MiB CLI / 256 KiB
HTTP body), non-object and out-of-range JSON is rejected with a tidy message,
and error responses never leak a traceback. Output is Jinja-autoescaped and
served with
X-Content-Type-Options,X-Frame-Options,Referrer-Policy, and a restrictiveContent-Security-Policy. - No secrets are read or logged.
.envonly holds a default host/port.
For any real deployment beyond a laptop, put a real WSGI server and authentication in front of it.
cd key-kettle
PYTHONPATH=src python -m pytest -qTests are fast and offline (no network, no sleeps): the Flask endpoints are exercised via the in-process test client. Coverage spans the analyzer (scoring, grade boundaries, full-checklist invariants), the policy validator (range and type rejection, weak-default fill, key warnings), the CLI (exit codes, JSON purity, file/stdin handling, size caps, the loopback gate), and the app (400-not-500 on bad input, 413 on oversize, security headers).
Layout
key-kettle/
src/key_kettle/
policy.py # PasswordPolicy dataclass, validation, sample baselines
analyzer.py # 11 controls → scored PolicyReport
ui.py # shared rich theme, banner, report renderer
cli.py # sample-policy / analyze / serve
app.py # Flask JSON input + report UI (hardened)
templates/
tests/
Put the policy on to boil and see what survives. Most password rules are folklore passed between compliance documents, and they optimise for looking strict rather than being strong. NIST rewrote its guidance in 2017; a startling number of policies never got the memo. This audits yours against 11 controls drawn from NIST, OWASP and CISA — and cites the source for every complaint, so the fix argument is already won before the meeting starts.
One of ten defensive tools rebuilt to a single standard — understand, reimagine, build the CLI, harden, document, verify. The goal was never to change what these tools are, but to take each one as far as it could honestly go on correctness, safety and the experience of the person actually running it at 2am.
Purpose. A password-policy auditor that reads as a complete, triage-ordered checklist rather than a pile of complaints. Every one of 11 NIST/OWASP/CISA-aligned controls reports pass or fail with a cited fix.
How it works. It ingests a policy (JSON file, stdin, or a generated
sample-policy), validates and coerces the fields, evaluates all 11 controls,
scores and grades the result, and sorts recommendations worst-first. The same
data drives the CLI table, the JSON, and the local report UI.
Standout CLI. A color-coded, triage-ordered table with a score/grade summary
panel; the banner goes to stderr for machine commands so stdout stays pipeable;
--policy - reads from stdin so sample-policy | analyze --policy - works; and
--fail-under SCORE turns analyze into a tunable CI gate.
Key improvements. Functional: the audit now reports every control as
pass or fail (11 total) instead of only listing problems, so you see complete
posture; strict PolicyError validation on ranges and types; real-world type
coercion (boolean strings, 0/""/null as disabled rotation); broadened
GOOD_HASHES to include argon2i/argon2d. Security: serve refuses non-loopback
binds without --allow-nonlocal (the UI has no auth and evaluates pasted JSON)
and refuses --debug off loopback; MAX_CONTENT_LENGTH 256 KiB plus an in-handler
re-check; CLI caps reads at 1 MiB; strict CSP and the standard header set.
Example.
PYTHONPATH=src python -m key_kettle.cli sample-policy --strong \
| PYTHONPATH=src python -m key_kettle.cli analyze --policy -These are defensive tools for systems, files, networks, and people you own or are explicitly authorized to assess. Read this before you run anything:
- Authorization is not optional. Phishing simulations, network scans, IP
reputation lookups, and web-app probing all touch other people's systems or
data. Get written authorization first. Several tools refuse to act until you
assert it (
--yes,--authorized-training,--i-have-authorization,--i-am-authorized, and friends). - Safe by default. Every web UI binds to
127.0.0.1(loopback). Egress, live sends, and active scans are gated behind explicit flags — dry-run, passive, or refuse-and-warn is always the default. - No accidental foot-guns. Flask
--debug(the Werkzeug interactive debugger is remote code execution on any traceback) is warned about loudly and refused entirely off loopback. Untrusted input is size-capped, validated, and rendered inertly so a hostile file or log line can't crash — or take over — your terminal. - No secrets in output. API keys stay in request headers, passwords come from the environment, and nothing sensitive is logged or printed.
These are not offensive tooling. They contain no exploits, no credential harvesters, and no payloads. If a tool could be misused, it is built to resist that misuse by construction.
python -m venv .venv && source .venv/bin/activate
pip install -e .
pytest -q # 49 tests, no network, no sleeps
key-kettle --helpReleased under the MIT License. See LICENSE.
