Skip to content

Commit 09937a1

Browse files
config to throttle all Scan endpoints, all IPs (#2646)
* all-paths, all-ips config * remove declarations we don't want * more precise match exprs - see https://cloud.google.com/armor/docs/rules-language-reference * use default allow in preview * config and versionedExpr must be specified at same time * Rules deletedWith the containing SecurityPolicy; much faster deletion --------- Signed-off-by: Stephen Compall <stephen.compall@digitalasset.com>
1 parent f46cfa4 commit 09937a1

File tree

3 files changed

+81
-74
lines changed

3 files changed

+81
-74
lines changed

cluster/deployment/config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ monitoring:
8888
cloudArmor:
8989
enabled: false
9090
allRulesPreviewOnly: true
91+
publicEndpoints:
92+
publicScan:
93+
hostname: scan.sv-2.scratcha.global.canton.network.digitalasset.com
94+
pathPrefix: /api/scan
95+
throttleAcrossAllEndpointsAllIps:
96+
withinIntervalSeconds: 60
97+
maxRequestsBeforeHttp429: 0
9198
multiValidator:
9299
postgresPvcSize: '100Gi'
93100
resources:

cluster/pulumi/infra/src/cloudArmor.ts

Lines changed: 65 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33
import * as gcp from '@pulumi/gcp';
44
import * as pulumi from '@pulumi/pulumi';
5+
import * as _ from 'lodash';
56
import { CLUSTER_BASENAME } from '@lfdecentralizedtrust/splice-pulumi-common';
67

78
import * 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+
3329
export 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
*/
127115
function 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
},

cluster/pulumi/infra/src/config.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,15 @@ const CloudArmorConfigSchema = z.object({
6565
.object({})
6666
.catchall(
6767
z.object({
68-
domain: z.string(),
69-
// TODO (DACH-NY/canton-network-internal#2115) more config
68+
rulePreviewOnly: z.boolean().default(false),
69+
hostname: z.string().regex(/^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/, 'valid DNS hostname'),
70+
pathPrefix: z.string().regex(/^\/[^"]*$/, 'HTTP request path starting with /'),
71+
throttleAcrossAllEndpointsAllIps: z.object({
72+
withinIntervalSeconds: z.number().positive(),
73+
maxRequestsBeforeHttp429: z
74+
.number()
75+
.min(0, '0 to disallow requests or positive to allow'),
76+
}),
7077
})
7178
)
7279
.default({}),

0 commit comments

Comments
 (0)