Skip to content

Allow custom csp header#5477

Open
TheSyscall wants to merge 78 commits intomainfrom
allow-custom-csp-header-5333
Open

Allow custom csp header#5477
TheSyscall wants to merge 78 commits intomainfrom
allow-custom-csp-header-5333

Conversation

@TheSyscall
Copy link
Copy Markdown

@TheSyscall TheSyscall commented Mar 12, 2026

Taking over #5337 (#5337 (comment)) and implementing an override for a completely custom CSP-Header.

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

Styling for this table is still WIP.

requires Icinga/ipl-web#358
requires Icinga/ipl-web#361

closes #5337
closes #5333

@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
Copy link
Copy Markdown
Member

@Al2Klimov Al2Klimov left a comment

Choose a reason for hiding this comment

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

13 review comments should be enough for a Friday 13th. :D

$useCustomCsp = $this->getPopulatedValue('use_custom_csp', 'n') === 'y';
if ($useCustomCsp) {
$this->addHtml((new Callout(
CalloutType::Warning,
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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Already listed as a requirement

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

class CspConfigForm extends CompatForm
Copy link
Copy Markdown
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.

Copy link
Copy Markdown
Author

@TheSyscall TheSyscall Mar 25, 2026

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.

@TheSyscall TheSyscall requested a review from Al2Klimov March 19, 2026 08:58
);

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

@flourish86
Copy link
Copy Markdown
Contributor

flourish86 commented Mar 23, 2026

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
@TheSyscall TheSyscall force-pushed the allow-custom-csp-header-5333 branch from 711be73 to 8c73a47 Compare March 25, 2026 09:27
@TheSyscall TheSyscall force-pushed the allow-custom-csp-header-5333 branch from be79bd9 to f25a499 Compare March 25, 2026 11:49
@TheSyscall TheSyscall requested a review from Al2Klimov March 25, 2026 14:02
@lippserd lippserd removed this from the 2.13 milestone Mar 25, 2026
Copy link
Copy Markdown
Member

@Al2Klimov Al2Klimov left a comment

Choose a reason for hiding this comment

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

(Nächste Aufgabe V. Don't hurry. I'm not even done, yet...)

My test setup

  1. Install Fedora 42
  2. Apply https://github.com/Al2Klimov/ansible-icinga-fedora
  3. Enable strict CSP
  4. Upgrade to https://git.icinga.com/packages/icinga-php-library/-/jobs/846553
  5. Downgrade to https://git.icinga.com/packages/icingaweb/-/jobs/846577

Preparation

Icinga Web 2 packages

https://git.icinga.com/packages/icingaweb/-/jobs/846577

Build icingaweb2 packages from allow-custom-csp-header-5333 branch.

ipl packages

https://git.icinga.com/packages/icinga-php-library/-/jobs/846553

  1. git -C ipl-web checkout origin/main
  2. git -C ipl-web merge --no-edit origin/cps origin/callouts
  3. git -C ipl-web switch -C $USER/review-csp
  4. git -C ipl-web push -fu
  5. cd icinga-php-library
  6. git checkout origin/main
  7. perl -pi -e 'if (m~ipl/web~) { s~main~$ENV{USER}/review-csp~ }' bin/make-snapshot.sh
  8. perl -pi -e 's~^(PHP_VERSION)=.*~$1=8.2~' bin/make-snapshot.sh
  9. bin/make-snapshot.sh $USER/review-csp
  10. git diff --stat origin/snapshot/nightly..HEAD
  11. git push -fu
  12. Build icinga-php-library packages from $USER/review-csp branch

}

try {
if ($config->get('csp_enable_dashboards', '1')) {
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.

When I upgrade IW2 with CSP already being enabled, ...

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes, they would be enabled by default. The way I understand @lippserd is that things should just work out of the box.
I would expect most people to have CSP disabled in their setup because of its current limitations.

Just so that I understand what you are saying, it should work like this:

  • CSP is off by default, if the admin enables it via the config form, all three of these toggles should automatically enable (despite no entry in the config) and be stored in their on state once the admin submits the form.
  • If CSP was already enabled beforehand, they should stay off (again no value in the configuration)

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.

Exactly! Then it would "just work out of the box" for people who enable it later.

Upgraders who already have CSP enabled did latter because the v2.13 behavior fits their needs. They simply don't expect this behavior change coming from behind through the cold kitchen.

}

try {
if ($config->get('csp_enable_navigation', '1')) {
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.

... wouldn't these two toggles suddenly be enabled "by default" and widen my CSP w/o asking?

(I mean, we could default-enable them IN THE FORM, while someone enables CSP at all, but here... ?)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

See above

'info-circle',
[
'class' => 'csp-policy-info',
'title' => t('This is an automatically generated nonce. Its value is unique per request.'),
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.

Unique per request? Are you sure? Because I toggle the CSP settings container reload and get the same nonce.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It is unique per initial request to icingaweb2 (without javascript XHR request).

If you know a better word, please suggest one.

  • request: not clear enough
  • session: I would say that refers to a user session (being logged in)
  • page load: icinga loads pages all the time, user might not know that (same problem as request)
  • leaving it out: But when is it generated?

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.

window reload maybe?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@flourish86 I'm not sure what the best wording for this is. Technically it is for each initial request that doesn't come from JavaScript.

@Al2Klimov Al2Klimov self-requested a review March 30, 2026 15:59
Co-authored-by: Alexander Aleksandrovič Klimov <alexander.klimov@icinga.com>
Copy link
Copy Markdown
Member

@Al2Klimov Al2Klimov left a comment

Choose a reason for hiding this comment

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

31 review comments at once should be enough for a Tuesday 31st. :)

$csps = Csp::load(new ConfigObject([
'csp_enable_modules' => '1',
'csp_enable_dashboards' => '1',
'csp_enable_navigation' => '1',
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.

Changes to the CSP and enabled modules already trigger page reloads. This is good.

In contrast, dashboard/navigation changes in the GUI don't trigger page reloads, even with these two toggles being enabled. This is bad.

After adding iframe?url=https://packages.icinga.com to the dashboard, Safari complains: "Refused to load https://packages.icinga.com/ because it does not appear in the frame-src directive of the Content Security Policy." I had to open the dev console to see it! And to reload the window to fix it.

Copy link
Copy Markdown
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 what the best way to solve this is. We can of course reload the page if the user edits one of these two. But that doesn't solve the problem for other users if the item ends up being shared between users.

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.

That's fine.


if (count($rows) === 0) {
$this->add(
HtmlElement::create('p', ['class' => 'csp-form-hint'], sprintf('No %s policies found.', $title))
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.

Not only this reads "No Modules policies found", this is a nightmare for translation.


if ($field !== null) {
$this->addElement('checkbox', $field, [
'label' => sprintf($this->translate('Enable %s'), $title),
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.

Yet another nightmare for translation...

*
* @return void
*/
protected function addPolicyTable(
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.

Navigation literally says Type: "menu-item". In the UI. What about mapping such rather technical strings to translatable, human-readable text?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Menu items are extensible and modules can create new types if they desire. So while we can create a mapping it would only ever cover out own types.

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.

Types from extensions can be displayed literally, unless:

Please check whether we could extend the respective hook with an optional method providing a (possibly translated) display name.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

There is no hook for this. This is part of the modules configuration.php.
I will use the label attribute of the defined navigation type, it is the same as in the configuration screen for the user's navigation.

$this->addHtml((new Callout(
CalloutType::Warning,
$this->translate(
'Be aware that the custom CSP-Header completely overrides the automatically generated one.'
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 miss instructions for the nonce. Keep this in mind when you write the docs.

* @param string $module the module to load the CSP directive for
*/
public function __construct(
public readonly string $module,
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.

Yes, but consider making the whole class readonly instead (as in #5442) while on it.

{
public function __construct(
public readonly string $type,
public readonly NavigationItem $item,
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.

Yes, but consider making the whole class readonly instead (as in #5442).

class NavigationCspReason extends CspReason
{
public function __construct(
public readonly string $type,
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.

Sounds like an enum makes sense here. I'll let it up to you.

* @param string $name the name to display for CSP reason
*/
public function __construct(
public readonly string $name,
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.

Yes, but consider making the whole class readonly instead (as in #5442) while on it.

* Base class for CSP loaders.
* A loader is responsible for loading CSP directives from a specific source.
*/
abstract class CspLoader
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.

Whenever I have the completely free choice of making a class or interface, I make an interface. (PHP doesn't support multiple inheritance.)

{
$user = Auth::getInstance()->getUser();
if ($user === null) {
throw new RuntimeException('No user logged in');
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 know this only ends up as log warning. But even for a warning, this is a too small "problem" IMAO. I mean, sure, no user logged in. And... now what? Panic? Is this such an uncommon scenario where:

  • Admin allows dashboard CSP
  • User visits the login page to log in


$auth = Auth::getInstance();
if (! $auth->isAuthenticated()) {
throw new RuntimeException('No user logged in');
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 wonder whether a simple return would be enough...

public static function fromCsp(Csp $csp, CspReason $reason): static
{
$instance = new static($reason);
$instance->directives = $csp->directives;
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.

Yes, directives is protected. Hence accessible. I wonder, though, whether this -and Csp as base class- is actually necessary or a dead simple readonly class with two properties would do it as well. You decide.

In Icinga/icinga2#10397, to be fair, I also enthusiastically used advanced inheritance tricks, but colleagues told me to just make it a property. (K.I.S.S.)

Comment on lines +54 to +55
$header = static::getHeader();
$response->setHeader('Content-Security-Policy', $header, true);
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.

Suggested change
$header = static::getHeader();
$response->setHeader('Content-Security-Policy', $header, true);
$response->setHeader('Content-Security-Policy', static::getHeader(), true);

$section['use_custom_csp'] = $this->getValue('use_custom_csp');
$section['custom_csp'] = $this->getValue('custom_csp');

$this->changed = ! empty(array_diff_assoc(
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.

Strange, my CSP page reloads on EVERY saving...

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

7 participants