Class: CloudflareAccessGate::Gate
- Inherits:
-
Object
- Object
- CloudflareAccessGate::Gate
- Defined in:
- lib/cloudflare_access_gate/gate.rb
Overview
Rack middleware that gates a dashboard (typically Sidekiq::Web) behind
Cloudflare Access. It reads the Cf-Access-Jwt-Assertion header injected by
the Cloudflare Access application in front of the host and, when JWT
validation is enabled, verifies the token's signature (against the team's
JWKS), issuer, audience, and expiry before letting the request through.
Everything is fail-closed: the gate stays on and rejects requests unless a flag is explicitly set to the string 'false', and a missing audience, missing team domain, or unavailable JWKS results in a 403 rather than an open door.
ENV contract:
ENABLE_CLOUDFLARE_GATE - 'false' disables the gate entirely
ENABLE_CLOUDFLARE_JWT_VALIDATION - 'false' skips signature/iss/aud checks
CLOUDFLARE_ACCESS_TEAM_DOMAIN - your Cloudflare Access team slug
The audience is passed as a constructor option, not read from ENV.
This class is plain Rack: no Rails, ActiveSupport, or logging library is required at runtime.
Constant Summary collapse
- DEFAULT_LEEWAY =
Clock drift allowance for exp/nbf, matching the 60s Cloudflare itself allows when validating tokens. (
iatis not verified: ruby-jwt leaves verify_iat off by default and its iat check ignores this leeway.) 60- REQUIRED_CLAIMS =
verify_*options only check a claim when it is present, so a correctly signed token that simply omitsexpwould otherwise never expire. %w[exp iss aud].freeze
- FORBIDDEN_BODY =
'Forbidden'- FORBIDDEN_HEADERS =
Header names are lowercase because the Rack 3 SPEC requires it ("uppercase character in header name" is a Rack::Lint error there). Rack 2 accepts lowercase too — HTTP header names are case-insensitive — so one spelling is correct on both.
content-lengthis set explicitly so the response is complete for servers that don't add it. { 'content-type' => 'text/plain', 'content-length' => FORBIDDEN_BODY.bytesize.to_s }.freeze
Instance Attribute Summary collapse
-
#audience ⇒ Object
readonly
Returns the value of attribute audience.
-
#team_domain ⇒ Object
readonly
Returns the value of attribute team_domain.
Class Method Summary collapse
-
.enabled? ⇒ Boolean
Fail closed: the gate stays ON unless explicitly disabled with 'false'.
-
.jwt_validation_enabled? ⇒ Boolean
Fail closed: JWT validation stays ON unless explicitly disabled with 'false'.
Instance Method Summary collapse
- #call(env) ⇒ Object
-
#initialize(app, options = {}) ⇒ Gate
constructor
Options: audience: (required in practice) the Access application AUD tag team_domain: Access team slug; defaults to ENV leeway: clock drift allowance in seconds jwks_stale_grace: seconds a stale JWKS may be reused on refresh failure logger: per-instance logger override clock: callable returning monotonic seconds (for tests).
Constructor Details
#initialize(app, options = {}) ⇒ Gate
Options:
audience: (required in practice) the Access application AUD tag
team_domain: Access team slug; defaults to ENV
leeway: clock drift allowance in seconds
jwks_stale_grace: seconds a stale JWKS may be reused on refresh failure
logger: per-instance logger override
clock: callable returning monotonic seconds (for tests)
55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/cloudflare_access_gate/gate.rb', line 55 def initialize(app, = {}) @app = app @audience = [:audience] @team_domain = .fetch(:team_domain) { ENV.fetch('CLOUDFLARE_ACCESS_TEAM_DOMAIN', nil) } @leeway = .fetch(:leeway, DEFAULT_LEEWAY) @logger = [:logger] && StructuredLogger.wrap([:logger]) @jwks_cache_mutex = Mutex.new @jwks_options = { stale_grace: .fetch(:jwks_stale_grace, JwksCache::DEFAULT_STALE_GRACE), clock: .fetch(:clock, JwksCache::MONOTONIC_CLOCK) } end |
Instance Attribute Details
#audience ⇒ Object (readonly)
Returns the value of attribute audience.
46 47 48 |
# File 'lib/cloudflare_access_gate/gate.rb', line 46 def audience @audience end |
#team_domain ⇒ Object (readonly)
Returns the value of attribute team_domain.
46 47 48 |
# File 'lib/cloudflare_access_gate/gate.rb', line 46 def team_domain @team_domain end |
Class Method Details
.enabled? ⇒ Boolean
Fail closed: the gate stays ON unless explicitly disabled with 'false'.
80 81 82 |
# File 'lib/cloudflare_access_gate/gate.rb', line 80 def self.enabled? ENV.fetch('ENABLE_CLOUDFLARE_GATE', 'true') != 'false' end |
.jwt_validation_enabled? ⇒ Boolean
Fail closed: JWT validation stays ON unless explicitly disabled with 'false'.
85 86 87 |
# File 'lib/cloudflare_access_gate/gate.rb', line 85 def self.jwt_validation_enabled? ENV.fetch('ENABLE_CLOUDFLARE_JWT_VALIDATION', 'true') != 'false' end |
Instance Method Details
#call(env) ⇒ Object
68 69 70 71 72 73 74 75 76 77 |
# File 'lib/cloudflare_access_gate/gate.rb', line 68 def call(env) return @app.call(env) unless self.class.enabled? token = env['HTTP_CF_ACCESS_JWT_ASSERTION'] return forbidden('Missing access token', env) if blank?(token) return forbidden('Invalid access token', env) unless token_acceptable?(token) @app.call(env) end |