api_keys 0.4.0 — security hardening
This is a security-focused release. Production users should upgrade after reading the migration and behavior changes below.
Migration before deployment
Existing applications must add the bounded authentication lookup index before deploying 0.4.0:
bundle update api_keys
bin/rails generate api_keys:add_authentication_index
bin/rails db:migrateThe generated migration is idempotent and uses concurrent index operations on PostgreSQL where supported. It creates the (prefix, last4, digest_algorithm) lookup used to bound bcrypt candidate work.
Security and reliability highlights
- Cached entries are key-ID hints only. Every request reloads the database row and cryptographically verifies the presented token, so revocation, expiration, scope, environment, and deletion changes take effect immediately.
- SHA-256 and bcrypt keys can coexist across configuration changes. Token/digest/cost/prefix/candidate work is structurally bounded; bcrypt performs at most 32 candidate comparisons and fails closed on overfull sets.
- Plaintext credentials are removed from authentication logs, model/result inspection and serialization, cache values, async callback payloads, and newly written session payloads.
- One-time dashboard/session handoffs are now application-key-derived AES-256-GCM ciphertext, bound to the created key ID, purpose-bound, ten-minute-expiring, downgrade-safe, and deleted on first retrieval. Legacy 0.3.x handoffs remain readable only for upgrade continuity and are never newly written; because those old values had no timestamp, clear/rotate existing sessions at deploy time if immediate removal is required.
- Owner rows are pessimistically locked across quota validation and insertion for helper, association, and direct Active Record creation paths.
- Authentication identity is immutable; digest formats, scopes, metadata, type/environment configuration, public-token policy, and permission ceilings are validated and fail closed.
- The mounted dashboard now fails closed on missing/ineffective owner authentication, scopes every record lookup to its owner, hardens malformed/error paths, and applies credential-safe cache/referrer/frame/MIME/Permissions-Policy headers plus an enforcing nonce CSP.
- Production API-key authentication requires HTTPS by default. Query-string credentials remain disabled by default.
- Callback jobs carry only small serializable, credential-free context; queues resolve after initializers; timestamps are monotonic and counters atomic.
- CI and release supply chain now include a nine-combination Ruby/Rails matrix, dependency audits, Brakeman, CodeQL, dependency review, Dependabot, secret scanning/push protection, signed protected
main, full-SHA Actions, and RubyGems OIDC Trusted Publishing.
Issue #12 is fully resolved
All 13 v0.3.0 review findings are fixed or superseded in this release: raw debug-token logging, plaintext session handoffs, unbounded bcrypt scans, missing-scope status, the decoy parent controller setting, early-bound job queues, tenant/owner aliasing, non-revocable owner-deletion failures, quota races, per-request default stats jobs, redundant new-install owner indexes, development files in the gem, and low native coverage.
See the per-finding code/test/edge-case matrix in PR #23 and the original issue #12.
Behavior changes to review
- A valid key lacking a required scope now receives
403 Forbidden; missing or invalid credentials remain401 Unauthorized. - Encrypted handoffs add some session-cookie overhead. Applications already near Rails' cookie-size limit, or custom integrations using unusually long tokens, should test their complete session payload or use a server-side session store.
- Configure dashboard inheritance with
config.parent_controller = "Admin::ApplicationController"before engine controllers load. The old internal Engine setting remains a fallback only when the public setting is untouched. - Deleting an owner now removes all associated API-key rows, including non-revocable/public types. Direct key-level revoke/destroy protections remain.
last_used_atjobs are debounced for one minute by default when exact request counting is disabled. Setstats_update_interval = 0ornilfor per-request timestamps.track_requests_count = trueintentionally performs per-request stats work.- New typed keys require
key_type:ordefault_key_type; malformed/retired typed environments fail closed. Existing untyped legacy keys remain compatible. - Authentication identity fields can no longer be mutated after creation; rotate keys instead.
- Invalid expiration presets now raise instead of silently creating permanent keys.
- Authentication callbacks now receive credential-free context hashes and execute asynchronously once. Use a durable Active Job backend when delivery matters.
- Blank/malformed/out-of-policy scopes fail closed when a scope policy is enabled.
- Reassess every
public: truepermission ceiling. The gem validates a finite least-privilege shape, but only the host application can decide whether a business permission is safe for an untrusted client.
Compatibility and verification
- Gemspec compatibility: Ruby 3.1+ and Rails 6.1+.
- Actively security-tested matrix: Ruby 3.3, 3.4, and 4.0 × Rails 7.2, 8.0, and 8.1.
- Final native suite: 315 tests, 966 assertions, no failures/errors/skips; 85.99% line and 77.14% branch coverage.
- Root, dummy, and Docker dependency audits: no known vulnerabilities at release time.
- Brakeman and CodeQL: clean on the release commit.
- Published gem SHA-256:
008ade0316c35dbe8339318d0af20013441788d86d97cda3b8b2ab47b7bdd835. - Tag/commit:
v0.4.0→5d92121228b4a0b3bfa50e1beb308a2db7335d95(GitHub signature verified). - Trusted Publishing run: Release #31288696180.
Full details are in the 0.4.0 changelog and release PR.
No software is literally guaranteed 100% secure. Consuming applications remain responsible for TLS/proxy correctness, secure cookies/session stores, rate limiting, business authorization, durable queues, secure database/cache/log/backup operations, monitoring, and incident response.