Skip to content

Commit

Permalink
WIP: Add a source IP allowlist for user-facing services
Browse files Browse the repository at this point in the history
- Add database table and updates with an allowlist of IPs. This is
  currently structured as a table with one row, and an array of INETs as
  the allowlist.
- Add Nexus API for viewing / updating allowlist. Also does basic sanity
  checks on the list, most importantly that the source IP the request
  came from is _on_ the list.
- Add the allowlist to wicket example config file
- Add the allowlist into the rack initialization request, and insert it
  into the database during Nexus's internal server handling of that
  request.
- Read allowlist and convert to firewall rules when plumbing any service
  firewall rules to sled agents.
- Add method for waiting on this plumbing _before_ starting Nexus's
  external server, to ensure the IP allowlist is set before anything can
  reach Nexus.
  • Loading branch information
bnaecker committed May 2, 2024
1 parent 472d9f7 commit e3a3d5c
Show file tree
Hide file tree
Showing 32 changed files with 1,127 additions and 13 deletions.
2 changes: 1 addition & 1 deletion bootstore/src/schemes/v0/peer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ pub enum NodeRequestError {

#[error(
"Network config update failed because it is out of date. Attempted
update generation: {attempted_update_generation}, current generation:
update generation: {attempted_update_generation}, current generation:
{current_generation}"
)]
StaleNetworkConfig {
Expand Down
14 changes: 14 additions & 0 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
mod error;
pub mod http_pagination;
pub use crate::api::internal::shared::AllowedSourceIps;
pub use crate::api::internal::shared::SwitchLocation;
use crate::update::ArtifactHash;
use crate::update::ArtifactId;
Expand Down Expand Up @@ -847,6 +848,7 @@ impl JsonSchema for Hostname {
pub enum ResourceType {
AddressLot,
AddressLotBlock,
AllowedSourceIps,
BackgroundTask,
BgpConfig,
BgpAnnounceSet,
Expand Down Expand Up @@ -1431,6 +1433,18 @@ impl IpNet {
}
}
}

/// Return true if the provided address is contained in self.
///
/// This returns false if the address and the network are of different IP
/// families.
pub fn contains(&self, addr: IpAddr) -> bool {
match (self, addr) {
(IpNet::V4(net), IpAddr::V4(ip)) => net.contains(ip),
(IpNet::V6(net), IpAddr::V6(ip)) => net.contains(ip),
(_, _) => false,
}
}
}

impl From<ipnetwork::IpNetwork> for IpNet {
Expand Down
69 changes: 67 additions & 2 deletions common/src/api/internal/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use crate::{
address::NUM_SOURCE_NAT_PORTS,
api::external::{self, BfdMode, Name},
api::external::{self, BfdMode, IpNet, Name},
};
use ipnetwork::{IpNetwork, Ipv4Network, Ipv6Network};
use schemars::JsonSchema;
Expand Down Expand Up @@ -184,7 +184,7 @@ pub struct BgpConfig {

#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
pub struct BgpPeerConfig {
/// The autonomous sysetm number of the router the peer belongs to.
/// The autonomous system number of the router the peer belongs to.
pub asn: u32,
/// Switch port the peer is reachable on.
pub port: String,
Expand Down Expand Up @@ -422,3 +422,68 @@ impl fmt::Display for PortFec {
}
}
}

/// Description of source IPs allowed to reach rack services.
#[derive(
Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize,
)]
#[serde(rename_all = "snake_case", tag = "allow", content = "ips")]
pub enum AllowedSourceIps {
/// Allow traffic from any external IP address.
#[default]
Any,
/// Restrict access to a specific set of source IP addresses or subnets.
///
/// All others are prevented from reaching rack services.
List(Vec<IpNet>),
}

#[cfg(test)]
mod tests {
use crate::api::{
external::{IpNet, Ipv4Net, Ipv6Net},
internal::shared::AllowedSourceIps,
};
use ipnetwork::{Ipv4Network, Ipv6Network};
use std::net::{Ipv4Addr, Ipv6Addr};

#[test]
fn test_deserialize_allowed_source_ips() {
let parsed: AllowedSourceIps = serde_json::from_str(
"{ \"allow\" : \"list\", \"ips\" : [\"127.0.0.1\", \"10.0.0.0/24\", \"fd00::1/64\"]}",
)
.unwrap();
assert_eq!(
parsed,
AllowedSourceIps::List(vec![
IpNet::from(Ipv4Addr::LOCALHOST),
IpNet::V4(Ipv4Net(
Ipv4Network::new(Ipv4Addr::new(10, 0, 0, 0), 24).unwrap()
)),
IpNet::V6(Ipv6Net(
Ipv6Network::new(
Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 1),
64
)
.unwrap()
)),
])
);
}

#[test]
fn test_deserialize_unknown_string() {
serde_json::from_str::<AllowedSourceIps>("{\"allow\": \"wat\"}")
.expect_err(
"Should not be able to deserialize from unknown variant name",
);
}

#[test]
fn test_deserialize_any_into_allowed_external_ips() {
assert_eq!(
AllowedSourceIps::Any,
serde_json::from_str("{\"allow\": \"any\"}").unwrap(),
);
}
}
94 changes: 94 additions & 0 deletions nexus/db-model/src/allowed_source_ip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/5.0/.

// Copyright 2024 Oxide Computer Company

//! Database representation of allowed source IP address, for implementing basic
//! IP allowlisting.
use crate::schema::allowed_source_ip;
use crate::Generation;
use chrono::DateTime;
use chrono::Utc;
use ipnetwork::IpNetwork;
use nexus_types::external_api::params;
use nexus_types::external_api::views;
use omicron_common::api::external;
use omicron_common::api::external::IpNet;
use serde::Deserialize;
use serde::Serialize;
use uuid::Uuid;

/// Database model for an allowlist of source IP addresses.
#[derive(
Queryable, Insertable, Selectable, Clone, Debug, Serialize, Deserialize,
)]
#[diesel(table_name = allowed_source_ip)]
pub struct AllowedSourceIp {
pub id: Uuid,
pub time_created: DateTime<Utc>,
pub time_modified: DateTime<Utc>,
pub generation: Generation,
pub allowed_ips: Option<Vec<IpNetwork>>,
}

impl AllowedSourceIp {
/// Construct a new allowlist record.
pub fn new(id: Uuid, allowed_ips: external::AllowedSourceIps) -> Self {
let now = Utc::now();
let allowed_ips = match allowed_ips {
external::AllowedSourceIps::Any => None,
external::AllowedSourceIps::List(list) => {
Some(list.into_iter().map(Into::into).collect())
}
};
Self {
id,
time_created: now,
time_modified: now,
generation: Generation::new(),
allowed_ips,
}
}
}

#[derive(AsChangeset)]
#[diesel(table_name = allowed_source_ip, treat_none_as_null = true)]
pub struct AllowedSourceIpUpdate {
/// The new list of allowed IPs.
pub allowed_ips: Option<Vec<IpNetwork>>,
}

impl From<params::AllowedSourceIpsUpdate> for AllowedSourceIpUpdate {
fn from(params: params::AllowedSourceIpsUpdate) -> Self {
let allowed_ips = match params.allowed_ips {
external::AllowedSourceIps::Any => None,
external::AllowedSourceIps::List(list) => {
Some(list.into_iter().map(Into::into).collect())
}
};
Self { allowed_ips }
}
}

impl From<AllowedSourceIp> for views::AllowedSourceIps {
fn from(db: AllowedSourceIp) -> Self {
let allowed_ips = if let Some(list) = db.allowed_ips {
assert!(
!list.is_empty(),
"IP allowlist must not be empty, use NULL instead"
);
external::AllowedSourceIps::List(
list.into_iter().map(IpNet::from).collect(),
)
} else {
external::AllowedSourceIps::Any
};
Self {
time_created: db.time_created,
time_modified: db.time_modified,
allowed_ips,
}
}
}
2 changes: 2 additions & 0 deletions nexus/db-model/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ extern crate diesel;
extern crate newtype_derive;

mod address_lot;
mod allowed_source_ip;
mod bfd;
mod bgp;
mod block_size;
Expand Down Expand Up @@ -114,6 +115,7 @@ mod db {
pub use self::macaddr::*;
pub use self::unsigned::*;
pub use address_lot::*;
pub use allowed_source_ip::*;
pub use bfd::*;
pub use bgp::*;
pub use block_size::*;
Expand Down
10 changes: 10 additions & 0 deletions nexus/db-model/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1624,6 +1624,16 @@ table! {
}
}

table! {
allowed_source_ip (id) {
id -> Uuid,
time_created -> Timestamptz,
time_modified -> Timestamptz,
generation -> Int8,
allowed_ips -> Nullable<Array<Inet>>,
}
}

table! {
db_metadata (singleton) {
singleton -> Bool,
Expand Down
3 changes: 2 additions & 1 deletion nexus/db-model/src/schema_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use std::collections::BTreeMap;
///
/// This must be updated when you change the database schema. Refer to
/// schema/crdb/README.adoc in the root of this repository for details.
pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(55, 0, 0);
pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(56, 0, 0);

/// List of all past database schema versions, in *reverse* order
///
Expand All @@ -29,6 +29,7 @@ static KNOWN_VERSIONS: Lazy<Vec<KnownVersion>> = Lazy::new(|| {
// | leaving the first copy as an example for the next person.
// v
// KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"),
KnownVersion::new(56, "add-allowed-source-ips"),
KnownVersion::new(55, "add-lookup-sled-by-policy-and-state-index"),
KnownVersion::new(54, "blueprint-add-external-ip-id"),
KnownVersion::new(53, "drop-service-table"),
Expand Down
Loading

0 comments on commit e3a3d5c

Please sign in to comment.