Your Jekyll site feels secure because it's static, but you're actually vulnerable to DDoS attacks, content scraping, credential stuffing, and various web attacks. Static doesn't mean invincible. Attackers can overwhelm your GitHub Pages hosting, scrape your content, or exploit misconfigurations. The false sense of security is dangerous. You need layered protection combining Cloudflare's network-level security with Ruby-based security tools for your development workflow.

In This Article

Adopting a Security Mindset for Static Sites

Static sites have unique security considerations. While there's no database or server-side code to hack, attackers focus on: (1) Denial of Service through traffic overload, (2) Content theft and scraping, (3) Credential stuffing on forms or APIs, (4) Exploiting third-party JavaScript vulnerabilities, and (5) Abusing GitHub Pages infrastructure. Your security strategy must address these vectors.

Cloudflare provides the first line of defense at the network edge, while Ruby security gems help secure your development pipeline and content. This layered approach—network security, content security, and development security—creates a comprehensive defense. Remember, security is not a one-time setup but an ongoing process of monitoring, updating, and adapting to new threats.

Security Layers for Jekyll Sites

Security Layer Threats Addressed Cloudflare Features Ruby Gems
Network Security DDoS, bot attacks, malicious traffic DDoS Protection, Rate Limiting, Firewall rack-attack, secure_headers
Content Security XSS, code injection, data theft WAF Rules, SSL/TLS, Content Scanning brakeman, bundler-audit
Access Security Unauthorized access, admin breaches Access Rules, IP Restrictions, 2FA devise, pundit (adapted)
Pipeline Security Malicious commits, dependency attacks API Security, Token Management gemsurance, license_finder

Configuring Cloudflare's Security Suite for Jekyll

Cloudflare offers numerous security features. Configure these specifically for Jekyll:

1. SSL/TLS Configuration

# Configure via API
cf.zones.settings.ssl.edit(
  zone_id: zone.id,
  value: 'full'  # Full SSL encryption
)

# Enable always use HTTPS
cf.zones.settings.always_use_https.edit(
  zone_id: zone.id,
  value: 'on'
)

# Enable HSTS
cf.zones.settings.security_header.edit(
  zone_id: zone.id,
  value: {
    strict_transport_security: {
      enabled: true,
      max_age: 31536000,
      include_subdomains: true,
      preload: true
    }
  }
)

2. DDoS Protection

# Enable under attack mode via API
def enable_under_attack_mode(enable = true)
  cf.zones.settings.security_level.edit(
    zone_id: zone.id,
    value: enable ? 'under_attack' : 'high'
  )
end

# Configure rate limiting
cf.zones.rate_limits.create(
  zone_id: zone.id,
  threshold: 100,
  period: 60,
  action: {
    mode: 'ban',
    timeout: 3600
  },
  match: {
    request: {
      methods: ['_ALL_'],
      schemes: ['_ALL_'],
      url: '*.yourdomain.com/*'
    },
    response: {
      status: [200],
      origin_traffic: false
    }
  }
)

3. Bot Management

# Enable bot fight mode
cf.zones.settings.bot_fight_mode.edit(
  zone_id: zone.id,
  value: 'on'
)

# Configure bot management for specific paths
cf.zones.settings.bot_management.edit(
  zone_id: zone.id,
  value: {
    enable_js: true,
    fight_mode: true,
    whitelist: [
      'googlebot',
      'bingbot',
      'slurp'  # Yahoo
    ]
  }
)

Essential Ruby Security Gems for Jekyll

Secure your development and build process:

1. brakeman for Jekyll Templates

While designed for Rails, adapt Brakeman for Jekyll:

gem 'brakeman'

# Custom configuration for Jekyll
Brakeman.run(
  app_path: '.',
  output_files: ['security_report.html'],
  check_arguments: {
    # Check for unsafe Liquid usage
    check_liquid: true,
    # Check for inline JavaScript
    check_xss: true
  }
)

# Create Rake task
task :security_scan do
  require 'brakeman'
  
  tracker = Brakeman.run('.')
  puts tracker.report.to_s
  
  if tracker.warnings.any?
    puts "⚠️  Found #{tracker.warnings.count} security warnings"
    exit 1 if ENV['FAIL_ON_WARNINGS']
  end
end

2. bundler-audit

Check for vulnerable dependencies:

gem 'bundler-audit'

# Run in CI/CD pipeline
task :audit_dependencies do
  require 'bundler/audit/cli'
  
  puts "Auditing Gemfile dependencies..."
  Bundler::Audit::CLI.start(['check', '--update'])
  
  # Also check for insecure licenses
  Bundler::Audit::CLI.start(['check', '--license'])
end

# Pre-commit hook
task :pre_commit_security do
  Rake::Task['audit_dependencies'].invoke
  Rake::Task['security_scan'].invoke
  
  # Also run Ruby security scanner
  system('gem scan')
end

3. secure_headers for Jekyll

Generate proper security headers:

gem 'secure_headers'

# Configure for Jekyll output
SecureHeaders::Configuration.default do |config|
  config.csp = {
    default_src: %w['self'],
    script_src: %w['self' 'unsafe-inline' https://static.cloudflareinsights.com],
    style_src: %w['self' 'unsafe-inline'],
    img_src: %w['self' data: https:],
    font_src: %w['self' https:],
    connect_src: %w['self' https://cloudflareinsights.com],
    report_uri: %w[/csp-violation-report]
  }
  
  config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload"
  config.x_frame_options = "DENY"
  config.x_content_type_options = "nosniff"
  config.x_xss_protection = "1; mode=block"
  config.referrer_policy = "strict-origin-when-cross-origin"
end

# Generate headers for Jekyll
def security_headers
  SecureHeaders.header_hash_for(:default).map do |name, value|
    ""
  end.join("\n")
end

4. rack-attack for Jekyll Server

Protect your local development server:

gem 'rack-attack'

# config.ru
require 'rack/attack'

Rack::Attack.blocklist('bad bots') do |req|
  # Block known bad user agents
  req.user_agent =~ /(Scanner|Bot|Spider|Crawler)/i
end

Rack::Attack.throttle('requests by ip', limit: 100, period: 60) do |req|
  req.ip
end

use Rack::Attack
run Jekyll::Commands::Serve

Web Application Firewall Configuration

Configure Cloudflare WAF specifically for Jekyll:

# lib/security/waf_manager.rb
class WAFManager
  RULES = {
    'jekyll_xss_protection' => {
      description: 'Block XSS attempts in Jekyll parameters',
      expression: '(http.request.uri.query contains " {
      description: 'Block requests to GitHub Pages admin paths',
      expression: 'starts_with(http.request.uri.path, "/_admin") or starts_with(http.request.uri.path, "/wp-") or starts_with(http.request.uri.path, "/administrator")',
      action: 'block'
    },
    'scraper_protection' => {
      description: 'Limit request rate from single IP',
      expression: 'http.request.uri.path contains "/blog/"',
      action: 'managed_challenge',
      ratelimit: {
        characteristics: ['ip.src'],
        period: 60,
        requests_per_period: 100,
        mitigation_timeout: 600
      }
    },
    'api_protection' => {
      description: 'Protect form submission endpoints',
      expression: 'http.request.uri.path eq "/contact" and http.request.method eq "POST"',
      action: 'js_challenge',
      ratelimit: {
        characteristics: ['ip.src'],
        period: 3600,
        requests_per_period: 10
      }
    }
  }
  
  def self.setup_rules
    RULES.each do |name, config|
      cf.waf.rules.create(
        zone_id: zone.id,
        description: config[:description],
        expression: config[:expression],
        action: config[:action],
        enabled: true
      )
    end
  end
  
  def self.update_rule_lists
    # Subscribe to managed rule lists
    cf.waf.rule_groups.create(
      zone_id: zone.id,
      package_id: 'owasp',
      rules: {
        'REQUEST-941-APPLICATION-ATTACK-XSS': 'block',
        'REQUEST-942-APPLICATION-ATTACK-SQLI': 'block',
        'REQUEST-913-SCANNER-DETECTION': 'block'
      }
    )
  end
end

# Initialize WAF rules
WAFManager.setup_rules

Implementing Advanced Access Control

Control who can access your site:

1. Country Blocking

def block_countries(country_codes)
  country_codes.each do |code|
    cf.firewall.rules.create(
      zone_id: zone.id,
      action: 'block',
      priority: 1,
      filter: {
        expression: "(ip.geoip.country eq \"#{code}\")"
      },
      description: "Block traffic from #{code}"
    )
  end
end

# Block common attack sources
block_countries(['CN', 'RU', 'KP', 'IR'])

2. IP Allowlisting for Admin Areas

def allowlist_ips(ips, paths = ['/_admin/*'])
  ips.each do |ip|
    cf.firewall.rules.create(
      zone_id: zone.id,
      action: 'allow',
      priority: 10,
      filter: {
        expression: "(ip.src eq #{ip}) and (#{paths.map { |p| "http.request.uri.path contains \"#{p}\"" }.join(' or ')})"
      },
      description: "Allow IP #{ip} to admin areas"
    )
  end
end

# Allow your office IPs
allowlist_ips(['203.0.113.1', '198.51.100.1'])

3. Challenge Visitors from High-Risk ASNs

def challenge_high_risk_asns
  high_risk_asns = ['AS12345', 'AS67890']  # Known bad networks
  
  cf.firewall.rules.create(
    zone_id: zone.id,
    action: 'managed_challenge',
    priority: 5,
    filter: {
      expression: "(ip.geoip.asnum in {#{high_risk_asns.join(' ')}})"
    },
    description: "Challenge visitors from high-risk networks"
  )
end

Security Monitoring and Incident Response

Monitor security events and respond automatically:

# lib/security/incident_response.rb
class IncidentResponse
  def self.monitor_security_events
    events = cf.audit_logs.search(
      zone_id: zone.id,
      since: '-300',  # Last 5 minutes
      action_types: ['firewall_rule', 'waf_rule', 'access_rule']
    )
    
    events.each do |event|
      case event['action']['type']
      when 'firewall_rule_blocked'
        handle_blocked_request(event)
      when 'waf_rule_triggered'
        handle_waf_trigger(event)
      when 'access_rule_challenged'
        handle_challenge(event)
      end
    end
  end
  
  def self.handle_blocked_request(event)
    ip = event['request']['client_ip']
    path = event['request']['url']
    
    # Log the block
    SecurityLogger.log_block(ip, path, event['rule']['description'])
    
    # If same IP blocked 5+ times in hour, add permanent block
    if block_count_last_hour(ip) >= 5
      cf.firewall.rules.create(
        zone_id: zone.id,
        action: 'block',
        filter: { expression: "ip.src eq #{ip}" },
        description: "Permanent block for repeat offenses"
      )
      
      send_alert("Permanently blocked IP #{ip} for repeat attacks", :critical)
    end
  end
  
  def self.handle_waf_trigger(event)
    rule_id = event['rule']['id']
    
    # Check if this is a new attack pattern
    if waf_trigger_count(rule_id, '1h') > 50
      # Increase rule sensitivity
      cf.waf.rules.update(
        zone_id: zone.id,
        rule_id: rule_id,
        sensitivity: 'high'
      )
      
      send_alert("Increased sensitivity for WAF rule #{rule_id}", :warning)
    end
  end
  
  def self.auto_mitigate_ddos
    # Check for DDoS patterns
    request_rate = cf.analytics.dashboard(
      zone_id: zone.id,
      since: '-60'
    )['result']['totals']['requests']['all']
    
    if request_rate > 10000  # 10k requests per minute
      enable_under_attack_mode(true)
      enable_rate_limiting(true)
      
      send_alert("DDoS detected, enabled under attack mode", :critical)
    end
  end
end

# Run every 5 minutes
IncidentResponse.monitor_security_events
IncidentResponse.auto_mitigate_ddos

Automating Security Compliance

Automate security checks and reporting:

# Rakefile security tasks
namespace :security do
  desc "Run full security audit"
  task :audit do
    puts "🔒 Running security audit..."
    
    # 1. Dependency audit
    puts "Checking dependencies..."
    system('bundle audit check --update')
    
    # 2. Content security scan
    puts "Scanning content..."
    system('ruby security/scanner.rb')
    
    # 3. Configuration audit
    puts "Auditing configurations..."
    audit_configurations
    
    # 4. Cloudflare security check
    puts "Checking Cloudflare settings..."
    audit_cloudflare_security
    
    # 5. Generate report
    generate_security_report
    
    puts "✅ Security audit complete"
  end
  
  desc "Update all security rules"
  task :update_rules do
    puts "Updating security rules..."
    
    # Update WAF rules
    WAFManager.update_rule_lists
    
    # Update firewall rules based on threat intelligence
    update_threat_intelligence_rules
    
    # Update managed rules
    cf.waf.managed_rules.sync(zone_id: zone.id)
    
    puts "✅ Security rules updated"
  end
  
  desc "Weekly security compliance report"
  task :weekly_report do
    report = SecurityReport.generate_weekly
    
    # Email report
    SecurityMailer.weekly_report(report).deliver
    
    # Upload to secure storage
    upload_to_secure_storage(report)
    
    puts "✅ Weekly security report generated"
  end
end

# Schedule with whenever
every :sunday, at: '3am' do
  rake 'security:weekly_report'
end

every :day, at: '2am' do
  rake 'security:update_rules'
end

Implement security in layers. Start with basic Cloudflare security features (SSL, WAF). Then add Ruby security scanning to your development workflow. Gradually implement more advanced controls like rate limiting and automated incident response. Within a month, you'll have enterprise-grade security protecting your static Jekyll site.