Skip to content

feat(cdn): Add support for configuring WAF#1372

Open
matheuspolitano wants to merge 55 commits intostackitcloud:mainfrom
matheuspolitano:mp/cdn/feat/implement-configuring-waf
Open

feat(cdn): Add support for configuring WAF#1372
matheuspolitano wants to merge 55 commits intostackitcloud:mainfrom
matheuspolitano:mp/cdn/feat/implement-configuring-waf

Conversation

@matheuspolitano
Copy link
Copy Markdown
Contributor

@matheuspolitano matheuspolitano commented Apr 13, 2026

Description

https://jira.schwarz/browse/STACKITCDN-723

I want to add support for the waf configuration block in the CDN distribution resource,so that users can programmatically manage security tiers, paranoia levels, and granular rule overrides (Enabled/Disabled/Log-only) via Terraform.

Checklist

  • Issue was linked above
  • Code format was applied: make fmt
  • Examples were added / adjusted (see examples/ directory)
  • Docs are up-to-date: make generate-docs (will be checked by CI)
  • Unit tests got implemented or updated
  • Acceptance tests got implemented or updated (see e.g. here)
  • Unit tests are passing: make test (will be checked by CI)
  • No linter issues: make lint (will be checked by CI)

@matheuspolitano matheuspolitano requested a review from a team as a code owner April 13, 2026 13:30
@matheuspolitano matheuspolitano changed the title Mp/cdn/feat/implement configuring waf feat(cdn): Add support for configuring WAF Apr 13, 2026
@matheuspolitano
Copy link
Copy Markdown
Contributor Author

@Fyusel @marceljk Kindly prioritize the review of PR #1333, as the current pull request builds upon those changes.

@Manuelvaas Manuelvaas added has internal tracking issue and removed Stale PR is marked as stale due to inactivity. labels Apr 23, 2026
matheuspolitano and others added 5 commits April 24, 2026 09:50
Co-authored-by: Marcel Jacek <72880145+marceljk@users.noreply.github.com>
Co-authored-by: Marcel Jacek <72880145+marceljk@users.noreply.github.com>
Comment thread docs/data-sources/cdn_distribution.md Outdated
Comment thread docs/data-sources/cdn_distribution.md Outdated
Comment thread docs/resources/cdn_distribution.md Outdated
Comment thread stackit/internal/services/cdn/distribution/resource.go
@marceljk
Copy link
Copy Markdown
Contributor

@Fyusel @marceljk Kindly prioritize the review of PR #1333, as the current pull request builds upon those changes.

#1333 is merged now. Please rebase this PR

Comment thread stackit/internal/services/cdn/distribution/resource.go
Comment thread stackit/internal/services/cdn/distribution/resource.go Outdated
Comment on lines +994 to +1045
// Map WAF Update/Removal
if !utils.IsUndefined(configPlanModel.Waf) {
var wafModel wafConfig
diags := configPlanModel.Waf.As(ctx, &wafModel, basetypes.ObjectAsOptions{})
if diags.HasError() {
core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping WAF config")
return
}
wafPatch := cdnSdk.WafConfigPatch{
Mode: new(cdnSdk.WafMode(wafModel.Mode.ValueString())),
Type: new(cdnSdk.WafType(wafModel.Type.ValueString())),
AllowedHttpVersions: getSortedWafList(ctx, wafModel.AllowedHttpVersions),
AllowedRequestContentTypes: getSortedWafList(ctx, wafModel.AllowedRequestContentTypes),
AllowedHttpMethods: getSortedWafList(ctx, wafModel.AllowedHttpMethods),
EnabledRuleIds: getSortedWafList(ctx, wafModel.EnabledRuleIds),
DisabledRuleIds: getSortedWafList(ctx, wafModel.DisabledRuleIds),
LogOnlyRuleIds: getSortedWafList(ctx, wafModel.LogOnlyRuleIds),
EnabledRuleGroupIds: getSortedWafList(ctx, wafModel.EnabledRuleGroupIds),
DisabledRuleGroupIds: getSortedWafList(ctx, wafModel.DisabledRuleGroupIds),
LogOnlyRuleGroupIds: getSortedWafList(ctx, wafModel.LogOnlyRuleGroupIds),
EnabledRuleCollectionIds: getSortedWafList(ctx, wafModel.EnabledRuleCollectionIds),
DisabledRuleCollectionIds: getSortedWafList(ctx, wafModel.DisabledRuleCollectionIds),
LogOnlyRuleCollectionIds: getSortedWafList(ctx, wafModel.LogOnlyRuleCollectionIds),
}
if !utils.IsUndefined(wafModel.ParanoiaLevel) {
pl := cdnSdk.WafParanoiaLevel(wafModel.ParanoiaLevel.ValueString())
wafPatch.ParanoiaLevel = &pl
}
configPatch.Waf = &wafPatch
} else if !utils.IsUndefined(configStateModel.Waf) {
// User explicitly removed the WAF block from their terraform configuration
modeDisabled := cdnSdk.WafMode(cdnSdk.WAFMODE_DISABLED)
typeFree := cdnSdk.WafType(cdnSdk.WAFTYPE_FREE)

wafPatch := cdnSdk.WafConfigPatch{
Mode: &modeDisabled,
Type: &typeFree,
// Send empty arrays to clear rules, keeping the API happy
EnabledRuleIds: []string{},
DisabledRuleIds: []string{},
LogOnlyRuleIds: []string{},
EnabledRuleGroupIds: []string{},
DisabledRuleGroupIds: []string{},
LogOnlyRuleGroupIds: []string{},
EnabledRuleCollectionIds: []string{},
DisabledRuleCollectionIds: []string{},
LogOnlyRuleCollectionIds: []string{},
// Intentionally omitted (nil) to avoid the 422 Unprocessable Entity error:
// AllowedHttpVersions, AllowedRequestContentTypes, AllowedHttpMethods
}
configPatch.Waf = &wafPatch
}
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.

Why do you need the state here? Is it needed to set WAFMODE_DISABLED or can configPatch.Waf set to null to remove it?

Nevertheless, I think you can check here also configPlanModel.Waf.IsNull() instead of !utils.IsUndefined(configStateModel.Waf) and would have the same outcome but without the need accessing the state

Copy link
Copy Markdown
Contributor Author

@matheuspolitano matheuspolitano May 4, 2026

Choose a reason for hiding this comment

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

We are no longer persisting states once the WAF is removed, it is only disabled and change the type to free a pushed the API

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.

But for this you don't need the state. You can just set WafConfigPatch always to mode=disabled and type=free, when in the terraform config nothing related to Waf is defined.

Then you can remove the added stateModel and revert all renamings from Model -> PlanModel. I would actually prefer to avoid here, handling the state and config model, because this can lead in the future to issue when someone mixes them. And this resource is already really big.

Comment on lines +268 to +269
func SortedStringsToListValue(items []string) basetypes.ListValue {
if len(items) == 0 {
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.

Suggested change
func SortedStringsToListValue(items []string) basetypes.ListValue {
if len(items) == 0 {
func SortedStringsToListValue(items []string) basetypes.ListValue {
if items == nil {
return types.ListNull(types.StringType)
}
if len(items) == 0 {

I would return here a null list in case the string slice is also null. null list != empty list
This will also need some adjustment to the tests

"mode": schema.StringAttribute{
Required: true,
Description: schemaDescriptions["waf_mode"],
Validators: []validator.String{stringvalidator.OneOf(string(cdnSdk.WAFMODE_DISABLED), string(cdnSdk.WAFMODE_ENABLED), string(cdnSdk.WAFMODE_LOG_ONLY))},
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.

We have in our SDK also a slice with all the enum. Use it here instead of referencing each enum by hand

Suggested change
Validators: []validator.String{stringvalidator.OneOf(string(cdnSdk.WAFMODE_DISABLED), string(cdnSdk.WAFMODE_ENABLED), string(cdnSdk.WAFMODE_LOG_ONLY))},
Validators: []validator.String{stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(cdnSdk.AllowedWafModeEnumValues)...)},

"type": schema.StringAttribute{
Required: true,
Description: schemaDescriptions["waf_type"],
Validators: []validator.String{stringvalidator.OneOf(string(cdnSdk.WAFTYPE_PREMIUM), string(cdnSdk.WAFTYPE_FREE))},
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.

same here

"paranoia_level": schema.StringAttribute{
Optional: true,
Description: schemaDescriptions["waf_paranoia_level"],
Validators: []validator.String{stringvalidator.OneOf(string(cdnSdk.WAFPARANOIALEVEL_L1), string(cdnSdk.WAFPARANOIALEVEL_L2), string(cdnSdk.WAFPARANOIALEVEL_L3), string(cdnSdk.WAFPARANOIALEVEL_L4))},
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.

same here

Comment on lines +994 to +1045
// Map WAF Update/Removal
if !utils.IsUndefined(configPlanModel.Waf) {
var wafModel wafConfig
diags := configPlanModel.Waf.As(ctx, &wafModel, basetypes.ObjectAsOptions{})
if diags.HasError() {
core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping WAF config")
return
}
wafPatch := cdnSdk.WafConfigPatch{
Mode: new(cdnSdk.WafMode(wafModel.Mode.ValueString())),
Type: new(cdnSdk.WafType(wafModel.Type.ValueString())),
AllowedHttpVersions: getSortedWafList(ctx, wafModel.AllowedHttpVersions),
AllowedRequestContentTypes: getSortedWafList(ctx, wafModel.AllowedRequestContentTypes),
AllowedHttpMethods: getSortedWafList(ctx, wafModel.AllowedHttpMethods),
EnabledRuleIds: getSortedWafList(ctx, wafModel.EnabledRuleIds),
DisabledRuleIds: getSortedWafList(ctx, wafModel.DisabledRuleIds),
LogOnlyRuleIds: getSortedWafList(ctx, wafModel.LogOnlyRuleIds),
EnabledRuleGroupIds: getSortedWafList(ctx, wafModel.EnabledRuleGroupIds),
DisabledRuleGroupIds: getSortedWafList(ctx, wafModel.DisabledRuleGroupIds),
LogOnlyRuleGroupIds: getSortedWafList(ctx, wafModel.LogOnlyRuleGroupIds),
EnabledRuleCollectionIds: getSortedWafList(ctx, wafModel.EnabledRuleCollectionIds),
DisabledRuleCollectionIds: getSortedWafList(ctx, wafModel.DisabledRuleCollectionIds),
LogOnlyRuleCollectionIds: getSortedWafList(ctx, wafModel.LogOnlyRuleCollectionIds),
}
if !utils.IsUndefined(wafModel.ParanoiaLevel) {
pl := cdnSdk.WafParanoiaLevel(wafModel.ParanoiaLevel.ValueString())
wafPatch.ParanoiaLevel = &pl
}
configPatch.Waf = &wafPatch
} else if !utils.IsUndefined(configStateModel.Waf) {
// User explicitly removed the WAF block from their terraform configuration
modeDisabled := cdnSdk.WafMode(cdnSdk.WAFMODE_DISABLED)
typeFree := cdnSdk.WafType(cdnSdk.WAFTYPE_FREE)

wafPatch := cdnSdk.WafConfigPatch{
Mode: &modeDisabled,
Type: &typeFree,
// Send empty arrays to clear rules, keeping the API happy
EnabledRuleIds: []string{},
DisabledRuleIds: []string{},
LogOnlyRuleIds: []string{},
EnabledRuleGroupIds: []string{},
DisabledRuleGroupIds: []string{},
LogOnlyRuleGroupIds: []string{},
EnabledRuleCollectionIds: []string{},
DisabledRuleCollectionIds: []string{},
LogOnlyRuleCollectionIds: []string{},
// Intentionally omitted (nil) to avoid the 422 Unprocessable Entity error:
// AllowedHttpVersions, AllowedRequestContentTypes, AllowedHttpMethods
}
configPatch.Waf = &wafPatch
}
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.

But for this you don't need the state. You can just set WafConfigPatch always to mode=disabled and type=free, when in the terraform config nothing related to Waf is defined.

Then you can remove the added stateModel and revert all renamings from Model -> PlanModel. I would actually prefer to avoid here, handling the state and config model, because this can lead in the future to issue when someone mixes them. And this resource is already really big.

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.

5 participants