Skip to content

Allow custom csp header#5477

Open
TheSyscall wants to merge 6 commits into
mainfrom
allow-custom-csp-header-5333
Open

Allow custom csp header#5477
TheSyscall wants to merge 6 commits into
mainfrom
allow-custom-csp-header-5333

Conversation

@TheSyscall

@TheSyscall TheSyscall commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

This PR extends the Content Security Policy (CSP) support in Icinga Web 2. Previously the CSP header was a fixed, hardcoded string. It is now configurable and automatically assembled from multiple sources.

Note: This PR is an extension of #5337

Automatic CSP generation

The CSP header is now built dynamically from a set of loaders:

  • StaticCspLoader: provides the baseline system directives (default-src, style-src, font-src, img-src, frame-src)
  • DashboardCspLoader: adds frame-src entries for URLs configured in dashlets
  • NavigationCspLoader: adds frame-src entries for URLs configured in navigation items
  • ModuleCspLoader: invokes the new CspHook to collect directives declared by modules

Each loader produces AttributedCsp instances that carry a CspReason describing their origin. This metadata is surfaced in the config UI as a table showing where each directive comes from.

Custom CSP header

Administrators can opt out of automatic generation, either completely or for each category separately and supply a raw CSP string instead. The string supports a {style_nonce} placeholder that is replaced with the per-request nonce at runtime.

CspHook

A new CspHook lets modules register custom CSP directives without modifying core configuration. Modules implement getCspForUser(User $user) (user-specific additions, e.g. URLs stored per-user) and `getCspForAllUsers() (all directives that apply to any user on the icingaweb instance)

Configuration UI

A new CspConfigForm replaces the single checkbox that previously lived in the general application settings. It provides:

  • A toggle to enable or disable the CSP header
  • A toggle to switch between automatic generation and a custom header
  • Per-source toggles (modules, dashboards, navigation)
  • A custom CSP textarea for administrators to define a custom CSP header.
  • The current effective CSP displayed as a table grouped by source (module, dashboard item, navigation item, or system) with security analysis: each directive source is classified as secure, warning, or critical based on its keywords and schemes

Callouts in affected forms

DashletForm and NavigationItemForm now show an info callout when CSP is active, but the corresponding loader is disabled, so users understand why an external URL might be blocked.

Configuration

All new options live under [security] in config.ini:

Option Description
use_strict_csp Enable the CSP header
use_custom_csp Use the custom header instead of auto-generation
custom_csp The raw custom CSP string (supports {style_nonce})
csp_enable_modules Include module hook directives (default: 1)
csp_enable_dashboards Include dashlet frame-src entries (default: 1)
csp_enable_navigation Include navigation item frame-src entries (default: 1)

As well as adding a table below the form which displays the source of the automatically generated CSP-Header.

Requires

@TheSyscall TheSyscall requested a review from nilmerg March 12, 2026 13:02
@TheSyscall TheSyscall self-assigned this Mar 12, 2026
@cla-bot

This comment was marked as duplicate.

@TheSyscall TheSyscall requested a review from Al2Klimov March 12, 2026 13:03
@cla-bot

This comment was marked as duplicate.

1 similar comment
@cla-bot

This comment was marked as duplicate.

@cla-bot

This comment was marked as resolved.

Al2Klimov

This comment was marked as resolved.

@bobapple

Copy link
Copy Markdown
Member

@cla-bot check

@cla-bot cla-bot Bot added the cla/signed label Mar 13, 2026
@TheSyscall TheSyscall force-pushed the allow-custom-csp-header-5333 branch from 735beb2 to cdef4c4 Compare March 13, 2026 13:18
@TheSyscall

This comment was marked as resolved.

@TheSyscall TheSyscall requested a review from Al2Klimov March 13, 2026 13:25
@Al2Klimov Al2Klimov removed the request for review from nilmerg March 13, 2026 14:44
Al2Klimov

This comment was marked as resolved.

use ipl\Web\Compat\CompatForm;
use ipl\Web\Widget\Callout;

class CspConfigForm extends CompatForm

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This currently doesn't output the config.ini to the screen like the old ConfigForm did.
I think changing the form type to a CompatForm is still the right call, because we don't want to rely on Zend_Forms forever.
If this behavior essential, we should reimplement the behavior of ConfigFrom as a CompatForm instead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even in new modules we reimplemented this behavior: Icinga/icingadb-web#1269

So I'd not downgrade this functionality.

@TheSyscall TheSyscall Mar 25, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR for this is open at #5480

@TheSyscall TheSyscall requested a review from Al2Klimov March 16, 2026 11:59
Al2Klimov

This comment was marked as resolved.

Comment thread application/forms/Config/Security/CspConfigForm.php Outdated
@TheSyscall TheSyscall requested a review from Al2Klimov March 19, 2026 08:58
Comment thread public/css/icinga/widgets.less Outdated
Comment thread application/forms/Config/Security/CspConfigForm.php Outdated
);

$this->addPolicyTable(
t('Dashboard'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're concerned about #5477 (comment), you should be about this one too:

As not a IW2 dev, I'd have absolutely no clue that these are only my dashboards (if any!) and others may have their own.

Consider listing the dashboard-CSPs of all users, after all we have them (by name) in the preference store or something idk.

Module-provided dashboards should already be included as you should see all modules as admin. I guess they may contain external URLs as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we always have access to all users in something like LDAP.
Also listing all dashboards and menu entries for all users in an organization on every initial request seems pretty expensive.

If a module wants something whitelisted beyond what is automatically detected, it can already do that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We always have access to all users' dashboards and menu entries.

Imagine you make a custom CSP based on the generated one, but latter misses non-your dashboards.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extended Navigation- and DashboardCspLoader to optionally list all required CSP directives instead of only for the currently authenticated one.

An open question is if we should add a parameter to the Hook so that modules can handle this case as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't even imagine what the latter might be good for. But in doubt, add a such parameter. (See #5419 (comment).) Altering hooks afterwards is... wait a minute, you already know how it's like (Icinga/icingaweb2-module-pdfexport#88).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a $allUsers bool to Csp::getCsp method

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡

@lippserd What about instead having two methods in the hook? Or, better, just one for now and when we need an additional one, we provide a default implementation in the abstract hook class.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lippserd's solution to this problem is to give the user object to the Hook.
This should allow for easier testing, and maybe impersonation later.
Calling the hook without a user object would return the CSP for all users.

Comment thread application/forms/Config/General/CspConfigForm.php Outdated
Comment thread application/forms/Config/Security/CspConfigForm.php
Comment thread application/controllers/ConfigController.php Outdated
Comment thread library/Icinga/Web/Widget/CspConfigurationTable.php Outdated
Comment thread library/Icinga/Web/Widget/CspConfigurationTable.php Outdated
Comment thread library/Icinga/Web/Widget/CspConfigurationTable.php Outdated
Comment thread library/Icinga/Util/Csp.php Outdated
@flourish86

flourish86 commented Mar 23, 2026

Copy link
Copy Markdown
Contributor

So, here would be my recommendation as discussed.

The simplified version

Step 1

Strict CSP ist disabled
Frame 13

Step 2

Enable CSP to see a table of generated CSP dependending on system and module usage.
Frame 11

Step 3

Toggle Custom CSP. This disables the generated CSP
Frame 12

Improvements

  • Proper hierarchy for the csp elements
  • Make obvious, what is enabled and disabled with "opacity"
  • Keep the table visible for reference, even when custom csp overrides the generated table
  • Add heltexts to clarify how it works

Additionally

  • Not in the mockups: Add toggles for Dashboard and Navigation section

@flourish86

Copy link
Copy Markdown
Contributor

Advanced version of the editor

Rows can be toggled separately.

Frame 10

Disabled rows will fade out

Frame 14

Custom CSP sources can be added additionally

Frame 15

Generated CSP can be disabled completely

Only Custom CSP Rules will be used

Frame 16

@TheSyscall TheSyscall requested a review from Al2Klimov March 24, 2026 10:59
@lippserd lippserd added this to the 2.13 milestone Mar 24, 2026
Comment thread doc/20-Advanced-Topics.md Outdated
Comment thread library/Icinga/Security/Csp/Loader/ArrayCspLoader.php Outdated
@TheSyscall TheSyscall requested a review from raviks789 June 25, 2026 12:52
raviks789
raviks789 previously approved these changes Jun 26, 2026
@lippserd lippserd added this to the 2.14.0 milestone Jun 26, 2026
@TheSyscall TheSyscall force-pushed the allow-custom-csp-header-5333 branch from a07b626 to 9c1bb6b Compare June 26, 2026 12:34
@TheSyscall TheSyscall changed the base branch from main to config-form-5479 June 26, 2026 12:34
@TheSyscall TheSyscall force-pushed the allow-custom-csp-header-5333 branch from 9c1bb6b to fe9ed22 Compare June 26, 2026 13:00
@TheSyscall TheSyscall force-pushed the config-form-5479 branch 2 times, most recently from 97b1f15 to 9d50bc0 Compare June 29, 2026 07:10
@TheSyscall TheSyscall force-pushed the allow-custom-csp-header-5333 branch from fe9ed22 to 5a85fc1 Compare July 1, 2026 09:32
@TheSyscall TheSyscall force-pushed the allow-custom-csp-header-5333 branch 4 times, most recently from 08a5bf3 to aa70225 Compare July 1, 2026 11:35
Base automatically changed from config-form-5479 to main July 1, 2026 13:30

@jrauh01 jrauh01 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your arrays are completely mixed regarding alignment. Personally I would tend to align all of them.

Comment thread application/controllers/ConfigController.php Outdated
*
* @return void
*
* @throws SecurityException If the user lacks the permission for configuring the security configuration

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this. I think we do only document exceptions directly thrown by functions.

Comment thread application/forms/Config/Security/CspConfigForm.php Outdated
Comment thread application/forms/Config/Security/CspConfigForm.php Outdated
Comment thread application/forms/Config/Security/CspConfigForm.php Outdated
Comment thread application/forms/Config/Security/CspConfigForm.php Outdated
Comment thread application/forms/Config/Security/CspConfigForm.php Outdated
Comment thread application/forms/Config/Security/CspConfigForm.php
Comment thread application/forms/Config/Security/CspConfigForm.php
Comment thread application/forms/Config/Security/CspConfigForm.php Outdated
@TheSyscall TheSyscall force-pushed the allow-custom-csp-header-5333 branch from c3c7fee to d87813a Compare July 1, 2026 14:14
The hardcoded CSP directives are replaced with dynamic collection from
three sources: user-defined navigation iframe items, dashboard dashlets,
and a new CspDirectiveHook for modules to contribute their own
directives. This allows the generated CSP header to accommodate
user-configured external URLs rather than blocking them outright.

Co-authored-by: Davide Zeni <davidezeni02@gmail.com>
Co-authored-by: Alexander Rieß <alexander.riess@icinga.com>
@TheSyscall TheSyscall force-pushed the allow-custom-csp-header-5333 branch from d87813a to cf6621d Compare July 1, 2026 14:15

@jrauh01 jrauh01 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some general stuff regarding phpdocs:

  • The lines do not need to necessarily end on a period. They should in general be wrapped after a specific line length.
  • Please use imperative voice in phpdoc short description.
  • In @return statements please do not write "Returns ...".
  • Please do not use types like null|string. Rather use ?string.

I already suggested changes in some places, but please check all docs on your own to fix this.

Comment thread application/forms/Config/Security/CspConfigForm.php Outdated
Comment thread application/forms/Config/Security/CspConfigForm.php Outdated
Comment thread application/forms/Config/Security/CspConfigForm.php Outdated
Comment thread application/forms/Config/Security/CspConfigForm.php
Comment thread application/forms/Config/Security/CspConfigForm.php Outdated
Comment thread library/Icinga/Util/Csp.php Outdated
Comment thread library/Icinga/Util/Csp.php Outdated
Comment thread library/Icinga/Util/Csp.php Outdated
Comment thread library/Icinga/Util/Csp.php Outdated
Comment thread library/Icinga/Util/Csp.php Outdated

@jrauh01 jrauh01 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some general stuff regarding phpdocs:

  • The lines do not need to necessarily end on a period. They should in general be wrapped after a specific line length.
  • Please use imperative voice in phpdoc short description.
  • In @return statements please do not write "Returns ...".
  • Please do not use types like null|string. Rather use ?string.

I already suggested changes in some places, but please check all docs on your own to fix this.

Replaces ad-hoc fetching logic with a CspLoader interface and concrete
implementations for arrays, dashboards, navigation items and modules.
Each directive set is wrapped in an AttributedCsp that pairs the Csp
instance with a CspReason, tracking which entity (pane, navigation item,
module) contributed it. This attribution is required for a planned
config UI to explain to admins why a specific directive is present in
the generated header.
@TheSyscall TheSyscall force-pushed the allow-custom-csp-header-5333 branch from d4f3768 to 05f681f Compare July 2, 2026 09:07
Delegates all directive fetching in Csp to the new CspLoader
abstractions. CspDirectiveHook, which returned a raw array, is replaced
by CspHook, which returns a typed Csp instance per-user and for all
users. The Csp class drops the singleton pattern for the CSP instance
and gains isEnabled(), isDashboardEnabled() and isNavigationEnabled()
helpers that read from config.ini, making feature checks consistent
across the codebase.
Moves CSP settings out of the General configuration form into a new
Security tab under Configuration > Application, gated by a new
config/security permission. The new CspConfigForm lets admins toggle the
CSP header, control which directive sources (modules, dashboards,
navigation) feed the generated policy, and optionally supply a custom
CSP string that fully overrides it. The editor analyses the custom input
and flags unsafe keywords ('unsafe-inline', 'unsafe-eval') and schemes
(http:, blob:).
Adds a Security Configuration section to 03-Configuration.md covering
all new config.ini keys (use_custom_csp, custom_csp, csp_enable_modules,
csp_enable_dashboards, csp_enable_navigation). Expands the CSP section
in 20-Advanced-Topics.md to explain per-source controls, the CspHook
extension point and the custom CSP override, including the {style_nonce}
placeholder. Adds a CspHook reference entry with an example
implementation to 60-Hooks.md.
When the CSP header is enabled but dashboards or navigation are not
included in the generated policy, editing a dashlet or navigation item
now shows an informational callout explaining that external URLs may not
render correctly and pointing the user to the Security configuration
page. Without this hint the breakage is silent and hard to connect to
the CSP settings.
@TheSyscall TheSyscall force-pushed the allow-custom-csp-header-5333 branch from 05f681f to b596eab Compare July 2, 2026 09:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow customization of the CSP

8 participants