yaml_csp_config: Rails content security policy configuration in YAML

What?

A gem for Rails 6+ that allows you to specify your content security policy (CSP) in a YAML file, instead of using the Rails DSL.

Why?

The YAML configuration is potentially more structured, and easier to read and maintain than using the Ruby DSL with conditional logic on env vars and so on.

Also config of the CSP becomes similar to configuring other things in Rails, such as the database, via YAML files.

Features

  • Configure your CSP in YAML
    • Use anchors/aliases to avoid duplicated blocks of URLs between different policy directives
    • Create Rails env specific configurations (eg directives only for development)
  • Extend the content security policy configuration via environment variables. Useful for deployed environments where the CSP is different per deployment. 1) configure a specific addition for a specific directive or 2) specify the name of a group of configurations to be applied.
  • The YAML file can contain ERB

Example

Below is an example of a security policy in YAML and Rails DSL.

In YAML (with this gem):

config/content_security_policy.yml

self_and_data_uri_policy: &SELF_AND_DATA
  - :self
  - :data

google_static_hosts: &GOOGLE_STATIC
  - https://*.googleapis.com
  - https://*.gstatic.com

content_security_policy:
  # Base config
  report_uri: "/csp-violation-report-endpoint"

  default_src: :self

  object_src: :none

  font_src: 
    - :self
    - *GOOGLE_STATIC
    - https://fonts.gstatic.com

  style_src: 
    - *SELF_AND_DATA
    - :unsafe_inline

  img_src:
    - *SELF_AND_DATA
    - *GOOGLE_STATIC
    - https://s3.amazonaws.com

  script_src:
    - :self
    - https://cdnjs.cloudflare.com
    - https://www.google-analytics.com
    - https://maps.googleapis.com

  connect_src:
    - :self

development:
  img_src:
    - http://localhost:3035

  script_src:
    - http://localhost:3035

  connect_src:
    - http://localhost:3035
    - ws://localhost:3000
    - ws://localhost:3035
    - ws://127.0.0.1:35729

review_apps:
  connect_src: 
    - wss://*.herokuapp.com

Equivalent in Ruby DSL:

config/initializers/content_security_policy.rb

GOOGLE_STATIC = ["https://*.googleapis.com", "https://*.gstatic.com"].freeze

CSP_SCRIPT_HOSTS = %w[
  https://cdnjs.cloudflare.com
  https://www.google-analytics.com
  https://maps.googleapis.com
].freeze

CSP_FONT_HOSTS = (["https://fonts.gstatic.com"] + GOOGLE_STATIC).freeze

CSP_IMAGE_HOSTS =  (["https://s3.amazonaws.com"] + GOOGLE_STATIC).freeze

CSP_WEBPACKER_HOST = "http://localhost:3035"

CSP_DEV_CONNECT_SRC = %w[
  http://localhost:3035
  ws://localhost:3000
  ws://localhost:3035
  ws://127.0.0.1:35729
].freeze

CSP_REVIEW_CONNECT_SRC = %w[
  wss://*.herokuapp.com
].freeze

Rails.application.config.content_security_policy do |policy|
  policy.report_uri("/csp-violation-report-endpoint")

  policy.default_src(:self)

  policy.object_src(:none)

  policy.font_src(:self, *CSP_FONT_HOSTS)

  policy.style_src(:self, :data, :unsafe_inline)

  if Rails.env.development?
    policy.img_src(:self, :data, CSP_WEBPACKER_HOST, *CSP_IMAGE_HOSTS)

    policy.script_src(:self, :unsafe_eval, CSP_WEBPACKER_HOST, *CSP_SCRIPT_HOSTS)

    policy.connect_src(:self, *CSP_DEV_CONNECT_SRC)
  else
    policy.img_src(:self, :data, *CSP_IMAGE_HOSTS)

    policy.script_src(:self, *CSP_SCRIPT_HOSTS)

    if ENV["IN_REVIEW_APP"].present?
      policy.connect_src(:self, *CSP_REVIEW_CONNECT_SRC)
    else
      policy.connect_src(:self)
    end
  end
end

# ...

Installation

Add to your Gemfile:

gem 'yaml_csp_config'

Or install it yourself as:

$ gem install yaml_csp_config

Then run the generator to add the initializer

rails g yaml_csp_config:install

Usage

YAML file format

Note: The YAML file can also be an ERB template.

The file must contain at at least the 'base' configuration group, containing the base or common CSP configuration.

This key of this group by default is content_security_policy but can be configured via the yaml_config_base_key config value in the initializer.

Directive configurations are then specified as keys named after the directive (see YamlCspConfig::YamlLoader::DIRECTIVES for a list) and then either an array of policy values, or a single value (note that if you use aliases you may end up creating nested arrays of values this is no problem as it will be flattened). Values can either be strings or symbols.

# example
content_security_policy:
  object_src: :none
  connect_src:
    - :self
  font_src: *SELF_AND_DATA
  script_src:
    - :self
    - *GOOGLE
  img_src: "host"

The file can contain any number of other configuration groups. If the group is named after an environment of your Rails application it will be mixed in automatically if the application is running in that environment.

Adding to configuration based on current Rails environment

A configuration group named after rails environment will be mixed in in that environment:

# example
development:
  connect_src: "host.dev"
test:
  connect_src: "host.test"

Adding a named configuration group using an environment variable

The name of the environment variable that can be set with the name of the group to add is by default CSP_CONFIGURATION_GROUP_KEY. It can be changed using the configuration variable default_env_var_group_key from the initializer.

for example given the following environment variables set on the application's environmentY

CSP_CONFIGURATION_GROUP_KEY=staging_app

the following configuration group will be mixed in:

# example
staging_app:
  connect_src: "host.staging"

Adding to configuration based with environment variables

The CSP configuration can also be extended directly by environment variables. The environment variable names are prefixed with a standard prefix. This prefix is by default CSP_CONFIGURATION_ADDITIONS_. It can be changed using the configuration variable default_env_var_additions_key_prefix from the initializer.

After the prefix comes the name of the directive in uppercase. The value of the environment variable will then be added automatically to the configuration of that directive.

For example:

CSP_CONFIGURATION_ADDITIONS_SCRIPT_SRC=host.cdn

will add host.cdn to the script_src directive.

Note this extends ActionDispatch::ContentSecurityPolicy.load_from_file

YamlCspConfig extends ActionDispatch::ContentSecurityPolicy with a method to load configuration from a YAML file. By default the initializer will add the load_from_file instance method and call it on initialisation.

If you wish instead to call it explicitly make sure to comment it out from the initializer.

Run type check (RBS & steep)

First copy the signatures for Rails from https://github.com/pocke/rbs_rails/tree/master/assets/sig to the project sig/rbs_rails directory. Then run

bundle exec steep check

Run tests

 ./bin/test

License

The gem is available as open source under the terms of the MIT License.

Contributing

Contributors welcome! Any contribution appreciated Pull requests, issues, and feature requests.

Contributors

Stephen Ierodiaconou