API Key Hashing
Context and Problem Statement
The proxy authenticates machine-to-machine clients via API keys. How should API keys be stored and compared? Storing plaintext keys in configuration or memory would leak secrets if the configuration is exposed (logs, env dumps, version control).
Decision Drivers
- API keys are long-lived credentials — a leak has lasting impact
- Configuration files may be committed to version control or visible in container env
- Key comparison must be constant-time-safe against timing attacks (though SHA-256 digest comparison mitigates this naturally)
- The hashing scheme must be simple enough for operators to generate hashes offline (e.g.,
echo -n "key" | sha256sum)
Considered Options
- Store plaintext keys and compare directly
- Store bcrypt/argon2 hashes (password-style)
- Store SHA-256 hex digests
Decision Outcome
Chosen option: "Store SHA-256 hex digests", because it provides a strong one-way transform while remaining simple to generate and fast to verify.
How it works
- Configuration stores only the SHA-256 hex digest of each API key
- At validation time, the incoming key is hashed with SHA-256 and compared against stored digests
- Plaintext keys are never persisted — not in config, not in memory after hashing
# Generating a key hash for configuration:
# echo -n "my-api-key" | sha256sum
# → a1b2c3d4...
# InMemoryApiKeyValidator stores only hashes:
class InMemoryApiKeyValidator:
def add_key(self, api_key: str, principal: Principal) -> None:
key_hash = hashlib.sha256(api_key.encode()).hexdigest()
self._keys[key_hash] = principal
def validate(self, api_key: str) -> KeyValidationResult:
key_hash = hashlib.sha256(api_key.encode()).hexdigest()
principal = self._keys.get(key_hash)
...
Why not bcrypt/argon2?
- API keys are high-entropy random strings (unlike passwords) — dictionary attacks are not a concern
- bcrypt/argon2 are intentionally slow, adding latency to every authenticated request
- SHA-256 is sufficient for high-entropy secrets and is available in Python's stdlib
Consequences
- Good, because plaintext keys never appear in stored configuration
- Good, because SHA-256 is fast — negligible overhead per request
- Good, because operators can generate hashes with standard CLI tools (
sha256sum,openssl dgst) - Neutral, because if the hash store is leaked, an attacker cannot reverse the keys (one-way)
- Bad, because SHA-256 provides no brute-force protection for low-entropy keys — operators must generate strong keys
Confirmation
InMemoryApiKeyValidatorstores only hashes;add_key()hashes before storageApiKeyEntryDtoconfiguration model has akey_hashfield (notkeyorapi_key)- Test
test_hashing_is_sha256verifies the hash matcheshashlib.sha256
More Information
- Related: ADR-0007 In-Memory Adapters
- Affected files:
src/hdc_auth_proxy/adapters/memory/api_key_validator.py,src/hdc_auth_proxy/adapters/dto/api_key.py