Class: CloudflareAccessGate::Gate

Inherits:
Object
  • Object
show all
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. (iat is 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 omits exp would 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-length is 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

Class Method Summary collapse

Instance Method Summary collapse

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, options = {})
  @app = app
  @audience = options[:audience]
  @team_domain = options.fetch(:team_domain) { ENV.fetch('CLOUDFLARE_ACCESS_TEAM_DOMAIN', nil) }
  @leeway = options.fetch(:leeway, DEFAULT_LEEWAY)
  @logger = options[:logger] && StructuredLogger.wrap(options[:logger])
  @jwks_cache_mutex = Mutex.new
  @jwks_options = {
    stale_grace: options.fetch(:jwks_stale_grace, JwksCache::DEFAULT_STALE_GRACE),
    clock: options.fetch(:clock, JwksCache::MONOTONIC_CLOCK)
  }
end

Instance Attribute Details

#audienceObject (readonly)

Returns the value of attribute audience.



46
47
48
# File 'lib/cloudflare_access_gate/gate.rb', line 46

def audience
  @audience
end

#team_domainObject (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'.

Returns:

  • (Boolean)


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'.

Returns:

  • (Boolean)


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