-
Notifications
You must be signed in to change notification settings - Fork 71
THREESCALE- 6512: Allow custom CSP #4185
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
e750b59 to
82a9346
Compare
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
a3f1e2e to
0a95ae6
Compare
| @@ -0,0 +1,33 @@ | |||
| base: &default | |||
| enabled: true | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit strange that in the default config it's enabled, and in each environment, which inherits from it, it's also explicitly set as enabled: true :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I am wondering... Could it make sense to have enabled and report_only at each level (admin vs dev portal) rather than globally? Because the customer might desire to only override the configs for e.g. dev portal, and not admin portal?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, I did it here: a0de8cd
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually meant something like:
base: &default
admin_portal:
enabled: true
report_only: false
policy:
default_src: ["'self'"]
# others
developer_portal:
enabled: true
report_only: false
policy:
default_src: [ "*", "data:", "mediastream:", "blob:", "filesystem:", "ws:", "wss:", "'unsafe-eval'", "'unsafe-inline'" ]
WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WDYT?
No objections: 07418e8
| end | ||
|
|
||
| def call(env) | ||
| request = ActionDispatch::Request.new(env) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit complicated for me... because I have no idea how this is supposed to work. But it seems that most of the logic is similar to what https://github.com/rails/rails/blob/v7.1.5.2/actionpack/lib/action_dispatch/http/content_security_policy.rb#L35 does.
I guess it's fine... It's unfortunate though that we (apparently) cannot reuse the existing logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, basically, I created this class and added it to the developer portal middleware stack.
- On startup, a new instance will be initialized.
- On every request, the
callmethod will be called.
Our call method is called by the previous middleware in the stack. By calling @app.call(env) we yield control to the next middleware in the stack, and eventually to the controller.
This class basically generates the headers once on startup and then adds them to each request. We can't reuse the existing middleware because that one takes the CSP policy from the Rails global CSP configuration, but we need to take it from our yaml file.
However, the Rails CSP middleware is in fact installed also in the stack, so we are calling it anyway, that's why we have this snippet:
unless request.format.html?
request.content_security_policy = false
return @app.call(env)
endWhen the request is HTML, we handle it; when it's not, we don't, and ensure the rails middlware doesn't handle it neither.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Admin portal: Yaml -> Rails Global CSP config -> Rails CSP middleware
- Dev portal: Yaml -> Our dev portal CSP middleware
|
I'm wondering, does this need to be per instance or per tenant? I assume the admin/master portals need to be per instance because we don't have custom stuff in there. For dev portals, it would make some sense to be individual. Just asking whether this makes practical sense. It will be a little more user friendly if configured from UI but given it will probably rarely be used, perhaps doesn't make much practical sense... thinking out loud. |
Yeah it would be good to be per tenant, specially the dev portal ones. But that would require more effort: Adding columns to the settings table, or maybe a new table; adapt models, creating the API endpoints or UI + Controllers... Do you want to create an issue? |
Only if you think that the benefit will outweigh the effort. Otherwise we can go like this and see if requests come. |
It would be useful for SaaS, of course. Not sure about On premises. Do on premises clients have more than one tenant usually? I wouldn't do it only for SaaS |
What this PR does / why we need it:
We previously added an all-allowed policy for convenience (#3861), but we provided no way to configure any other CSP policy. This PR accepts a new config file under
config/content_security_policy.ymlwhere porta loads its CSP policy from. Example:The PR consists basically in four changes:
application.rbIf no CSP config file, it will fall back to the previous all-allowed policy. When present, it will apply
admin_portal_policyTo master and provider portals, anddeveloper_portal_policyto developer portals.In fact,
admin_portal_policyis set on rails as global policy, so if nodeveloper_portal_policyis provided, admin portal CSP will be applied to dev portal as well.By default, I'm setting the all-allowed policy to dev portal anyway, because we can't know what clients will publish there. About admin portal, I'm setting a more restrictive defaults that would be valid for the whole portal, tests pass, however I might have missed something, let's see.
By default the CSP policy is enabled in all environments, including
developmentandtest, the reason is for future developments that could introduce new CSP violations to be revealed before they reach production.Additional comments
At the beginning I discarded allowing different CSP policy for admin and developer portal, because this CSP cusomization only makes sense for Dev portal actually, since clients can't decide the contents of the admin portal, other than allowing the CDN url. However, if we only have one global policy, whatever clients set for their developer portals would affect also the admin portal, over which they have no control. So I finally opted for allowing different policies.
There are a few features I discarded for different reasons:
1. Add support for new
report-todirectiveWe support
report-uri(docs), which is marked as deprecated, however, its replacementreport-tois not supported by rails yet (docs only mentionreport_uri, also, the:report_tomethod is not defined in the class).The CSP directive and the required HTTP header are also not widely supported for all browsers yet, e.g. not supported at all in Firefox.
I could add a small implementation, via middleware, but I think it's better to just use what rails provide today, that way the code will be easier to maintain.
2. Use
secure_headersgem rather than Rails builtin support.We are currently depending on the secure_headers gem. And that gem allows to add support for CSP. However, its CSP support doesn't offer any advantage over rails builtin support, so I opted for Rails, for better maintainability. Also, we would need to update the gem to a newer major version in order to get the same features rails provides.
Which issue(s) this PR fixes
https://issues.redhat.com/browse/THREESCALE-6512
Verification steps
Content-Security-PolicyHTTP header is actually set to what is configured in the yaml file.enabledtofalse. The CSP header should contain the old all-allowed policy.