Skip to content

Commit 71fe60a

Browse files
authored
Limit number of firewall rules per VPC (#5914)
Closes #5662 Closes #6032 - [x] Max firewall rules - [x] Enforce max length on filters (hosts, protocols, and ports) and targets too - [x] Improve doc comments on firewall rules types and API endpoint
1 parent ca0e1ea commit 71fe60a

7 files changed

Lines changed: 400 additions & 73 deletions

File tree

common/src/api/external/mod.rs

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,7 +1467,7 @@ pub enum RouterRouteKind {
14671467
/// its destination.
14681468
#[derive(ObjectIdentity, Clone, Debug, Deserialize, Serialize, JsonSchema)]
14691469
pub struct RouterRoute {
1470-
/// common identifying metadata
1470+
/// Common identifying metadata
14711471
#[serde(flatten)]
14721472
pub identity: IdentityMetadata,
14731473
/// The ID of the VPC Router to which the route belongs
@@ -1483,22 +1483,22 @@ pub struct RouterRoute {
14831483
/// A single rule in a VPC firewall
14841484
#[derive(ObjectIdentity, Clone, Debug, Deserialize, Serialize, JsonSchema)]
14851485
pub struct VpcFirewallRule {
1486-
/// common identifying metadata
1486+
/// Common identifying metadata
14871487
#[serde(flatten)]
14881488
pub identity: IdentityMetadata,
1489-
/// whether this rule is in effect
1489+
/// Whether this rule is in effect
14901490
pub status: VpcFirewallRuleStatus,
1491-
/// whether this rule is for incoming or outgoing traffic
1491+
/// Whether this rule is for incoming or outgoing traffic
14921492
pub direction: VpcFirewallRuleDirection,
1493-
/// list of sets of instances that the rule applies to
1493+
/// Determine the set of instances that the rule applies to
14941494
pub targets: Vec<VpcFirewallRuleTarget>,
1495-
/// reductions on the scope of the rule
1495+
/// Reductions on the scope of the rule
14961496
pub filters: VpcFirewallRuleFilter,
1497-
/// whether traffic matching the rule should be allowed or dropped
1497+
/// Whether traffic matching the rule should be allowed or dropped
14981498
pub action: VpcFirewallRuleAction,
1499-
/// the relative priority of this rule
1499+
/// The relative priority of this rule
15001500
pub priority: VpcFirewallRulePriority,
1501-
/// the VPC to which this rule belongs
1501+
/// The VPC to which this rule belongs
15021502
pub vpc_id: Uuid,
15031503
}
15041504

@@ -1511,29 +1511,29 @@ pub struct VpcFirewallRules {
15111511
/// A single rule in a VPC firewall
15121512
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
15131513
pub struct VpcFirewallRuleUpdate {
1514-
/// name of the rule, unique to this VPC
1514+
/// Name of the rule, unique to this VPC
15151515
pub name: Name,
1516-
/// human-readable free-form text about a resource
1516+
/// Human-readable free-form text about a resource
15171517
pub description: String,
1518-
/// whether this rule is in effect
1518+
/// Whether this rule is in effect
15191519
pub status: VpcFirewallRuleStatus,
1520-
/// whether this rule is for incoming or outgoing traffic
1520+
/// Whether this rule is for incoming or outgoing traffic
15211521
pub direction: VpcFirewallRuleDirection,
1522-
/// list of sets of instances that the rule applies to
1522+
/// Determine the set of instances that the rule applies to
1523+
#[schemars(length(max = 256))]
15231524
pub targets: Vec<VpcFirewallRuleTarget>,
1524-
/// reductions on the scope of the rule
1525+
/// Reductions on the scope of the rule
15251526
pub filters: VpcFirewallRuleFilter,
1526-
/// whether traffic matching the rule should be allowed or dropped
1527+
/// Whether traffic matching the rule should be allowed or dropped
15271528
pub action: VpcFirewallRuleAction,
1528-
/// the relative priority of this rule
1529+
/// The relative priority of this rule
15291530
pub priority: VpcFirewallRulePriority,
15301531
}
15311532

1532-
/// Updateable properties of a `Vpc`'s firewall
1533-
/// Note that VpcFirewallRules are implicitly created along with a Vpc,
1534-
/// so there is no explicit creation.
1533+
/// Updated list of firewall rules. Will replace all existing rules.
15351534
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
15361535
pub struct VpcFirewallRuleUpdateParams {
1536+
#[schemars(length(max = 1024))]
15371537
pub rules: Vec<VpcFirewallRuleUpdate>,
15381538
}
15391539

@@ -1553,19 +1553,24 @@ pub struct VpcFirewallRuleUpdateParams {
15531553
#[repr(transparent)]
15541554
pub struct VpcFirewallRulePriority(pub u16);
15551555

1556-
/// Filter for a firewall rule. A given packet must match every field that is
1557-
/// present for the rule to apply to it. A packet matches a field if any entry
1558-
/// in that field matches the packet.
1556+
/// Filters reduce the scope of a firewall rule. Without filters, the rule
1557+
/// applies to all packets to the targets (or from the targets, if it's an
1558+
/// outbound rule). With multiple filters, the rule applies only to packets
1559+
/// matching ALL filters. The maximum number of each type of filter is 256.
15591560
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)]
15601561
pub struct VpcFirewallRuleFilter {
1561-
/// If present, the sources (if incoming) or destinations (if outgoing)
1562-
/// this rule applies to.
1562+
/// If present, host filters match the "other end" of traffic from the
1563+
/// target’s perspective: for an inbound rule, they match the source of
1564+
/// traffic. For an outbound rule, they match the destination.
1565+
#[schemars(length(max = 256))]
15631566
pub hosts: Option<Vec<VpcFirewallRuleHostFilter>>,
15641567

15651568
/// If present, the networking protocols this rule applies to.
1569+
#[schemars(length(max = 256))]
15661570
pub protocols: Option<Vec<VpcFirewallRuleProtocol>>,
15671571

1568-
/// If present, the destination ports this rule applies to.
1572+
/// If present, the destination ports or port ranges this rule applies to.
1573+
#[schemars(length(max = 256))]
15691574
pub ports: Option<Vec<L4PortRange>>,
15701575
}
15711576

@@ -1599,8 +1604,11 @@ pub enum VpcFirewallRuleAction {
15991604
Deny,
16001605
}
16011606

1602-
/// A `VpcFirewallRuleTarget` is used to specify the set of `Instance`s to
1603-
/// which a firewall rule applies.
1607+
/// A `VpcFirewallRuleTarget` is used to specify the set of instances to which
1608+
/// a firewall rule applies. You can target instances directly by name, or
1609+
/// specify a VPC, VPC subnet, IP, or IP subnet, which will apply the rule to
1610+
/// traffic going to all matching instances. Targets are additive: the rule
1611+
/// applies to instances matching ANY target.
16041612
#[derive(
16051613
Clone,
16061614
Debug,
@@ -1760,7 +1768,7 @@ impl JsonSchema for L4PortRange {
17601768
title: Some("A range of IP ports".to_string()),
17611769
description: Some(
17621770
"An inclusive-inclusive range of IP ports. The second port \
1763-
may be omitted to represent a single port"
1771+
may be omitted to represent a single port."
17641772
.to_string(),
17651773
),
17661774
examples: vec!["22".into(), "6667-7000".into()],

nexus/db-model/src/vpc_firewall_rule.rs

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -211,20 +211,55 @@ pub struct VpcFirewallRule {
211211
pub priority: VpcFirewallRulePriority,
212212
}
213213

214+
/// Cap on the number of rules in a VPC
215+
///
216+
/// The choice of value is somewhat arbitrary, but the goal is to have a
217+
/// large number that customers are unlikely to actually hit, but which still
218+
/// meaningfully limits the ability to overload the DB with a single request.
219+
const MAX_FW_RULES_PER_VPC: usize = 1024;
220+
221+
/// Cap on targets and on each type of filter
222+
const MAX_FW_RULE_PARTS: usize = 256;
223+
224+
fn ensure_max_len<T>(
225+
items: &Vec<T>,
226+
label: &str,
227+
max: usize,
228+
) -> Result<(), external::Error> {
229+
if items.len() > max {
230+
let msg = format!("max length {}", max);
231+
return Err(external::Error::invalid_value(label, msg));
232+
}
233+
Ok(())
234+
}
235+
214236
impl VpcFirewallRule {
215237
pub fn new(
216238
rule_id: Uuid,
217239
vpc_id: Uuid,
218240
rule: &external::VpcFirewallRuleUpdate,
219-
) -> Self {
241+
) -> Result<Self, external::Error> {
220242
let identity = VpcFirewallRuleIdentity::new(
221243
rule_id,
222244
external::IdentityMetadataCreateParams {
223245
name: rule.name.clone(),
224246
description: rule.description.clone(),
225247
},
226248
);
227-
Self {
249+
250+
ensure_max_len(&rule.targets, "targets", MAX_FW_RULE_PARTS)?;
251+
252+
if let Some(hosts) = rule.filters.hosts.as_ref() {
253+
ensure_max_len(&hosts, "filters.hosts", MAX_FW_RULE_PARTS)?;
254+
}
255+
if let Some(ports) = rule.filters.ports.as_ref() {
256+
ensure_max_len(&ports, "filters.ports", MAX_FW_RULE_PARTS)?;
257+
}
258+
if let Some(protocols) = rule.filters.protocols.as_ref() {
259+
ensure_max_len(&protocols, "filters.protocols", MAX_FW_RULE_PARTS)?;
260+
}
261+
262+
Ok(Self {
228263
identity,
229264
vpc_id,
230265
status: rule.status.into(),
@@ -248,19 +283,20 @@ impl VpcFirewallRule {
248283
}),
249284
action: rule.action.into(),
250285
priority: rule.priority.into(),
251-
}
286+
})
252287
}
253288

254289
pub fn vec_from_params(
255290
vpc_id: Uuid,
256291
params: external::VpcFirewallRuleUpdateParams,
257292
) -> Result<Vec<VpcFirewallRule>, external::Error> {
258293
ensure_no_duplicates(&params)?;
259-
Ok(params
294+
ensure_max_len(&params.rules, "rules", MAX_FW_RULES_PER_VPC)?;
295+
params
260296
.rules
261-
.iter()
262-
.map(|rule| VpcFirewallRule::new(Uuid::new_v4(), vpc_id, rule))
263-
.collect())
297+
.into_iter()
298+
.map(|rule| VpcFirewallRule::new(Uuid::new_v4(), vpc_id, &rule))
299+
.collect()
264300
}
265301
}
266302

nexus/db-queries/src/db/datastore/vpc.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -221,20 +221,24 @@ impl DataStore {
221221
.map(|rule| (rule.name().clone(), rule))
222222
.collect::<BTreeMap<_, _>>();
223223

224-
fw_rules.entry(DNS_VPC_FW_RULE.name.clone()).or_insert_with(|| {
225-
VpcFirewallRule::new(
224+
// these have to be done this way because the contructor returns a result
225+
if !fw_rules.contains_key(&DNS_VPC_FW_RULE.name) {
226+
let rule = VpcFirewallRule::new(
226227
Uuid::new_v4(),
227228
*SERVICES_VPC_ID,
228229
&DNS_VPC_FW_RULE,
229-
)
230-
});
231-
fw_rules.entry(NEXUS_VPC_FW_RULE.name.clone()).or_insert_with(|| {
232-
VpcFirewallRule::new(
230+
)?;
231+
fw_rules.insert(DNS_VPC_FW_RULE.name.clone(), rule);
232+
}
233+
234+
if !fw_rules.contains_key(&NEXUS_VPC_FW_RULE.name) {
235+
let rule = VpcFirewallRule::new(
233236
Uuid::new_v4(),
234237
*SERVICES_VPC_ID,
235238
&NEXUS_VPC_FW_RULE,
236-
)
237-
});
239+
)?;
240+
fw_rules.insert(NEXUS_VPC_FW_RULE.name.clone(), rule);
241+
}
238242

239243
let rules = fw_rules
240244
.into_values()

nexus/src/external_api/http_entrypoints.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5378,7 +5378,6 @@ async fn vpc_subnet_list_network_interfaces(
53785378

53795379
// VPC Firewalls
53805380

5381-
// TODO Is the number of firewall rules bounded?
53825381
/// List firewall rules
53835382
#[endpoint {
53845383
method = GET,
@@ -5410,7 +5409,23 @@ async fn vpc_firewall_rules_view(
54105409
.await
54115410
}
54125411

5412+
// Note: the limits in the below comment come from the firewall rules model
5413+
// file, nexus/db-model/src/vpc_firewall_rule.rs.
5414+
54135415
/// Replace firewall rules
5416+
///
5417+
/// The maximum number of rules per VPC is 1024.
5418+
///
5419+
/// Targets are used to specify the set of instances to which a firewall rule
5420+
/// applies. You can target instances directly by name, or specify a VPC, VPC
5421+
/// subnet, IP, or IP subnet, which will apply the rule to traffic going to
5422+
/// all matching instances. Targets are additive: the rule applies to instances
5423+
/// matching ANY target. The maximum number of targets is 256.
5424+
///
5425+
/// Filters reduce the scope of a firewall rule. Without filters, the rule
5426+
/// applies to all packets to the targets (or from the targets, if it's an
5427+
/// outbound rule). With multiple filters, the rule applies only to packets
5428+
/// matching ALL filters. The maximum number of each type of filter is 256.
54145429
#[endpoint {
54155430
method = PUT,
54165431
path = "/v1/vpc-firewall-rules",

0 commit comments

Comments
 (0)