22// SPDX-License-Identifier: Apache-2.0
33import * as gcp from '@pulumi/gcp' ;
44import * as pulumi from '@pulumi/pulumi' ;
5+ import * as _ from 'lodash' ;
56import { CLUSTER_BASENAME } from '@lfdecentralizedtrust/splice-pulumi-common' ;
67
78import * as config from './config' ;
@@ -19,17 +20,12 @@ export interface ApiEndpoint {
1920 hostname : string ;
2021}
2122
22- export interface ThrottleConfig {
23- perIp : boolean ;
24- rate : number ; // Requests per minute
25- interval : number ; // Interval in seconds
26- }
27-
28- export type CloudArmorConfig = Pick < config . CloudArmorConfig , 'enabled' | 'allRulesPreviewOnly' > & {
23+ export type CloudArmorConfig = config . CloudArmorConfig & {
2924 predefinedWafRules ?: PredefinedWafRule [ ] ;
30- apiThrottles ?: ApiThrottleConfig [ ] ;
3125} ;
3226
27+ type ThrottleConfig = CloudArmorConfig [ 'publicEndpoints' ] ;
28+
3329export interface PredefinedWafRule {
3430 name : string ;
3531 action : 'allow' | 'deny' | 'throttle' ;
@@ -38,14 +34,6 @@ export interface PredefinedWafRule {
3834 sensitivityLevel ?: 'off' | 'low' | 'medium' | 'high' ;
3935}
4036
41- // TODO (DACH-NY/canton-network-internal#2115) replace this placeholder config
42- // with the real yaml structure we want to use
43- export interface ApiThrottleConfig {
44- endpoint : ApiEndpoint ;
45- throttle : ThrottleConfig ;
46- action : 'throttle' | 'ban' ;
47- }
48-
4937/**
5038 * Creates a Cloud Armor security policy
5139 * @param cac loaded configuration
@@ -75,7 +63,7 @@ export function configureCloudArmorPolicy(
7563 opts
7664 ) ;
7765
78- const ruleOpts = { ...opts , parent : securityPolicy } ;
66+ const ruleOpts = { ...opts , parent : securityPolicy , deletedWith : securityPolicy } ;
7967
8068 // Step 2: Add predefined WAF rules
8169 if ( cac . predefinedWafRules && cac . predefinedWafRules . length > 0 ) {
@@ -86,8 +74,8 @@ export function configureCloudArmorPolicy(
8674 addIpWhitelistRules ( /*securityPolicy, cac.allRulesPreviewOnly, ruleOpts*/ ) ;
8775
8876 // Step 4: Add throttling/banning rules for specific API endpoints
89- if ( cac . apiThrottles && cac . apiThrottles . length > 0 ) {
90- addThrottleAndBanRules ( securityPolicy , cac . apiThrottles , cac . allRulesPreviewOnly , ruleOpts ) ;
77+ if ( cac . publicEndpoints && ! _ . isEmpty ( cac . publicEndpoints ) ) {
78+ addThrottleAndBanRules ( securityPolicy , cac . publicEndpoints , cac . allRulesPreviewOnly , ruleOpts ) ;
9179 }
9280
9381 // Step 5: Add default deny rule
@@ -126,62 +114,59 @@ function addIpWhitelistRules(): void {
126114 */
127115function addThrottleAndBanRules (
128116 securityPolicy : gcp . compute . SecurityPolicy ,
129- apiThrottles : ApiThrottleConfig [ ] ,
117+ throttles : ThrottleConfig ,
130118 preview : boolean ,
131119 opts : pulumi . ResourceOptions
132120) : void {
133- apiThrottles . reduce ( ( priority , apiConfig ) => {
134- if ( priority >= THROTTLE_BAN_RULE_MAX ) {
135- throw new Error (
136- `Throttle rule priority ${ priority } exceeds maximum ${ THROTTLE_BAN_RULE_MAX } `
137- ) ;
138- }
139-
140- const { endpoint, throttle, action } = apiConfig ;
141- const ruleName = `${ action } ${ throttle . perIp ? '-per-ip' : '' } -${ endpoint . name } ` ;
142-
143- // Build the expression for path and hostname matching
144- const pathExpr = `request.path.matches('${ endpoint . path } ')` ;
145- const hostExpr = `request.headers['host'].matches('${ endpoint . hostname } ')` ;
146- const matchExpr = `${ pathExpr } && ${ hostExpr } ` ;
147-
148- new gcp . compute . SecurityPolicyRule (
149- ruleName ,
150- {
151- securityPolicy : securityPolicy . name ,
152- description : `${ action === 'throttle' ? 'Throttle' : 'Ban' } rule${ throttle . perIp ? ' per-IP' : '' } for ${ endpoint . name } API endpoint` ,
153- priority,
154- preview,
155- action : action === 'ban' ? 'rate_based_ban' : 'throttle' ,
156- match : {
157- expr : {
158- expression : matchExpr ,
159- } ,
160- } ,
161- rateLimitOptions : {
162- // ban point is banThreshold + ratelimit count; consider splitting up rather than doubling
163- ...( action === 'ban'
164- ? {
165- banDurationSec : 600 ,
166- banThreshold : {
167- count : throttle . rate ,
168- intervalSec : throttle . interval ,
169- } ,
170- }
171- : { } ) ,
172- enforceOnKey : throttle . perIp ? 'IP' : 'ALL' ,
173- rateLimitThreshold : {
174- count : throttle . rate ,
175- intervalSec : throttle . interval ,
121+ _ . sortBy ( Object . entries ( throttles ) , e => e [ 0 ] ) . reduce (
122+ ( priority , [ confEntryHead , singleServiceThrottle ] ) => {
123+ if ( priority >= THROTTLE_BAN_RULE_MAX ) {
124+ throw new Error (
125+ `Throttle rule priority ${ priority } exceeds maximum ${ THROTTLE_BAN_RULE_MAX } `
126+ ) ;
127+ }
128+
129+ const { hostname, pathPrefix, throttleAcrossAllEndpointsAllIps } = singleServiceThrottle ;
130+ // leave out the rule but consume the priority number if max is 0
131+ // this makes the pulumi update cleaner if toggling just one service
132+ if ( throttleAcrossAllEndpointsAllIps . maxRequestsBeforeHttp429 > 0 ) {
133+ const ruleName = `throttle-all-endpoints-all-ips-${ confEntryHead } ` ;
134+
135+ // Build the expression for path and hostname matching
136+ const pathExpr = `request.path.startsWith(R"${ pathPrefix } ")` ;
137+ const hostExpr = `request.headers['host'].matches(R"^${ _ . escapeRegExp ( hostname ) } (?::[0-9]+)?$")` ;
138+ const matchExpr = `${ pathExpr } && ${ hostExpr } ` ;
139+
140+ new gcp . compute . SecurityPolicyRule (
141+ ruleName ,
142+ {
143+ securityPolicy : securityPolicy . name ,
144+ description : `Throttle rule for all ${ confEntryHead } API endpoints` ,
145+ priority,
146+ preview : preview || singleServiceThrottle . rulePreviewOnly ,
147+ action : 'throttle' ,
148+ match : {
149+ expr : {
150+ expression : matchExpr ,
151+ } ,
152+ } ,
153+ rateLimitOptions : {
154+ enforceOnKey : 'ALL' ,
155+ rateLimitThreshold : {
156+ count : throttleAcrossAllEndpointsAllIps . maxRequestsBeforeHttp429 ,
157+ intervalSec : throttleAcrossAllEndpointsAllIps . withinIntervalSeconds ,
158+ } ,
159+ conformAction : 'allow' ,
160+ exceedAction : 'deny(429)' , // 429 Too Many Requests
161+ } ,
176162 } ,
177- conformAction : 'allow' ,
178- exceedAction : 'deny(429)' , // 429 Too Many Requests
179- } ,
180- } ,
181- opts
182- ) ;
183- return priority + RULE_SPACING ;
184- } , THROTTLE_BAN_RULE_MIN ) ;
163+ opts
164+ ) ;
165+ }
166+ return priority + RULE_SPACING ;
167+ } ,
168+ THROTTLE_BAN_RULE_MIN
169+ ) ;
185170}
186171
187172/**
@@ -192,15 +177,23 @@ function addDefaultDenyRule(
192177 preview : boolean ,
193178 opts : pulumi . ResourceOptions
194179) : void {
180+ // when you create a SecurityPolicy it has a default allow rule; we assume
181+ // that if you want all rules in preview, you *also* still want to allow
182+ // all traffic
183+ if ( preview ) {
184+ return ;
185+ }
195186 new gcp . compute . SecurityPolicyRule (
196187 'default-deny' ,
197188 {
198189 securityPolicy : securityPolicy . name ,
199190 description : 'Default rule to deny all other traffic' ,
200191 priority : DEFAULT_DENY_RULE_NUMBER ,
201- preview,
192+ // default rule cannot be in preview mode; google API gives 400 if you try
193+ preview : false ,
202194 action : 'deny' ,
203195 match : {
196+ versionedExpr : 'SRC_IPS_V1' ,
204197 config : {
205198 srcIpRanges : [ '*' ] ,
206199 } ,
0 commit comments