Skip to content

Part 4: Lock It Down

The lab is in the Internal zone with your own devices, so they can all reach each other. In this part you’ll give the lab its own zone, block it from Internal, and let your devices reach one lab server over SSH.

Create a Lab zone, and move the lab network into it:

src/lab.ts
import * as UnifiNetwork from "@alchemy-unifi/network";
import * as Config from "effect/Config";
import * as Effect from "effect/Effect";
export const Lab = Effect.gen(function* () {
const zone = yield* UnifiNetwork.FirewallZone("lab-zone", { name: "Lab" });
const network = yield* UnifiNetwork.Network("lab", {
management: "GATEWAY",
name: "Lab",
vlanId: 50,
enabled: true,
cellularBackupEnabled: false,
internetAccessEnabled: true,
isolationEnabled: false,
zone: UnifiNetwork.FirewallZone.INTERNAL,
zone: zone,
ipv4Configuration: {
autoScaleEnabled: false,
hostIpAddress: "192.168.50.1",
prefixLength: 24,
dhcpConfiguration: {
mode: "SERVER",
ipAddressRange: { start: "192.168.50.100", stop: "192.168.50.250" },
leaseTimeSeconds: 86400,
pingConflictDetectionEnabled: true,
},
},
});
yield* UnifiNetwork.WifiBroadcast("lab-wifi", {
type: "STANDARD",
name: "Lab",
enabled: true,
network,
securityConfiguration: {
type: "WPA2_PERSONAL",
passphrase: yield* Config.Redacted("LAB_WIFI_PASSPHRASE"),
fastRoamingEnabled: false,
},
broadcastingFrequenciesGHz: [2.4, 5],
hideName: false,
clientIsolationEnabled: false,
multicastToUnicastConversionEnabled: false,
uapsdEnabled: false,
channel2gLockedTo6: false,
dtimPeriod2gLockedTo3: false,
advertiseDeviceName: false,
arpProxyEnabled: false,
bssTransitionEnabled: true,
});
return { network };
return { network, zone };
});

Networks join zones from the network’s side, through zone.

Create src/firewall.ts, with a policy that blocks traffic from the lab to Internal:

src/firewall.ts
import * as UnifiNetwork from "@alchemy-unifi/network";
import type { Input } from "alchemy/Input";
import * as Effect from "effect/Effect";
export const Firewall = (lab: Input<UnifiNetwork.FirewallZoneReference>) =>
Effect.gen(function* () {
const { INTERNAL } = UnifiNetwork.FirewallZone;
yield* UnifiNetwork.FirewallPolicy("block-lab-to-internal", {
name: "Block Lab to Internal",
enabled: true,
action: { type: "BLOCK" },
source: { zone: lab },
destination: { zone: INTERNAL },
ipProtocolScope: { ipVersion: "IPV4_AND_IPV6" },
loggingEnabled: false,
});
});
alchemy.run.ts
import * as UnifiNetwork from "@alchemy-unifi/network";
import * as Alchemy from "alchemy";
import * as RemovalPolicy from "alchemy/RemovalPolicy";
import * as Effect from "effect/Effect";
import { Firewall } from "./src/firewall.ts";
import { Lab } from "./src/lab.ts";
export default Alchemy.Stack(
"UnifiLab",
{ providers: UnifiNetwork.providers(), state: UnifiNetwork.state() },
Effect.gen(function* () {
const lab = yield* Lab;
yield* Firewall(lab.zone);
yield* UnifiNetwork.Network("default", {
management: "GATEWAY",
name: "Default",
vlanId: 1,
enabled: true,
cellularBackupEnabled: true,
internetAccessEnabled: true,
isolationEnabled: false,
zone: UnifiNetwork.FirewallZone.INTERNAL,
ipv4Configuration: {
autoScaleEnabled: true,
hostIpAddress: "192.168.1.1",
prefixLength: 24,
dhcpConfiguration: {
mode: "SERVER",
ipAddressRange: { start: "192.168.1.6", stop: "192.168.1.254" },
leaseTimeSeconds: 86400,
domainName: "localdomain",
pingConflictDetectionEnabled: true,
},
},
}).pipe(RemovalPolicy.retain());
return { labNetworkId: lab.network.networkId };
}),
);

A traffic-matching list names addresses once, for any number of policies:

src/firewall.ts
import * as UnifiNetwork from "@alchemy-unifi/network";
import type { Input } from "alchemy/Input";
import * as Effect from "effect/Effect";
export const Firewall = (lab: Input<UnifiNetwork.FirewallZoneReference>) =>
Effect.gen(function* () {
const { INTERNAL } = UnifiNetwork.FirewallZone;
const servers = yield* UnifiNetwork.TrafficMatchingList("lab-servers", {
type: "IPV4_ADDRESSES",
name: "Lab servers",
items: [{ type: "IP_ADDRESS", value: "192.168.50.10" }],
});
yield* UnifiNetwork.FirewallPolicy("block-lab-to-internal", {
name: "Block Lab to Internal",
enabled: true,
action: { type: "BLOCK" },
source: { zone: lab },
destination: { zone: INTERNAL },
ipProtocolScope: { ipVersion: "IPV4_AND_IPV6" },
loggingEnabled: false,
});
});

Allow SSH from Internal to the lab servers, and block everything else from Internal to the lab:

src/firewall.ts
import * as UnifiNetwork from "@alchemy-unifi/network";
import type { Input } from "alchemy/Input";
import * as Effect from "effect/Effect";
export const Firewall = (lab: Input<UnifiNetwork.FirewallZoneReference>) =>
Effect.gen(function* () {
const { INTERNAL } = UnifiNetwork.FirewallZone;
const servers = yield* UnifiNetwork.TrafficMatchingList("lab-servers", {
type: "IPV4_ADDRESSES",
name: "Lab servers",
items: [{ type: "IP_ADDRESS", value: "192.168.50.10" }],
});
yield* UnifiNetwork.FirewallPolicy("block-lab-to-internal", {
name: "Block Lab to Internal",
enabled: true,
action: { type: "BLOCK" },
source: { zone: lab },
destination: { zone: INTERNAL },
ipProtocolScope: { ipVersion: "IPV4_AND_IPV6" },
loggingEnabled: false,
});
const allowSsh = yield* UnifiNetwork.FirewallPolicy("allow-ssh-to-lab-servers", {
name: "Allow SSH to the lab servers",
enabled: true,
action: { type: "ALLOW", allowReturnTraffic: true },
source: { zone: INTERNAL },
destination: {
zone: lab,
trafficFilter: {
type: "IP_ADDRESS",
ipAddressFilter: { type: "TRAFFIC_MATCHING_LIST", matchOpposite: false, trafficMatchingList: servers },
portFilter: { type: "PORTS", matchOpposite: false, items: [{ type: "PORT_NUMBER", value: 22 }] },
},
},
ipProtocolScope: {
ipVersion: "IPV4_AND_IPV6",
protocolFilter: { type: "PRESET", preset: { name: "TCP_UDP" } },
},
loggingEnabled: false,
});
const blockRest = yield* UnifiNetwork.FirewallPolicy("block-internal-to-lab", {
name: "Block Internal to the rest of the lab",
enabled: true,
action: { type: "BLOCK" },
source: { zone: INTERNAL },
destination: { zone: lab },
ipProtocolScope: { ipVersion: "IPV4_AND_IPV6" },
loggingEnabled: false,
});
});

allowReturnTraffic: true lets the replies back, so the SSH session works without a policy the other way.

Policies run in order, first match wins, and new ones go last. Put the allow before the block:

src/firewall.ts
import * as UnifiNetwork from "@alchemy-unifi/network";
import type { Input } from "alchemy/Input";
import * as Effect from "effect/Effect";
export const Firewall = (lab: Input<UnifiNetwork.FirewallZoneReference>) =>
Effect.gen(function* () {
const { INTERNAL } = UnifiNetwork.FirewallZone;
const servers = yield* UnifiNetwork.TrafficMatchingList("lab-servers", {
type: "IPV4_ADDRESSES",
name: "Lab servers",
items: [{ type: "IP_ADDRESS", value: "192.168.50.10" }],
});
yield* UnifiNetwork.FirewallPolicy("block-lab-to-internal", {
name: "Block Lab to Internal",
enabled: true,
action: { type: "BLOCK" },
source: { zone: lab },
destination: { zone: INTERNAL },
ipProtocolScope: { ipVersion: "IPV4_AND_IPV6" },
loggingEnabled: false,
});
const allowSsh = yield* UnifiNetwork.FirewallPolicy("allow-ssh-to-lab-servers", {
name: "Allow SSH to the lab servers",
enabled: true,
action: { type: "ALLOW", allowReturnTraffic: true },
source: { zone: INTERNAL },
destination: {
zone: lab,
trafficFilter: {
type: "IP_ADDRESS",
ipAddressFilter: { type: "TRAFFIC_MATCHING_LIST", matchOpposite: false, trafficMatchingList: servers },
portFilter: { type: "PORTS", matchOpposite: false, items: [{ type: "PORT_NUMBER", value: 22 }] },
},
},
ipProtocolScope: {
ipVersion: "IPV4_AND_IPV6",
protocolFilter: { type: "PRESET", preset: { name: "TCP_UDP" } },
},
loggingEnabled: false,
});
const blockRest = yield* UnifiNetwork.FirewallPolicy("block-internal-to-lab", {
name: "Block Internal to the rest of the lab",
enabled: true,
action: { type: "BLOCK" },
source: { zone: INTERNAL },
destination: { zone: lab },
ipProtocolScope: { ipVersion: "IPV4_AND_IPV6" },
loggingEnabled: false,
});
yield* UnifiNetwork.FirewallPolicyOrdering("internal-to-lab", {
source: INTERNAL,
destination: lab,
beforeSystemDefined: [allowSsh, blockRest],
});
});
Terminal window
npx alchemy deploy --stage home

From your laptop on the default network, ssh to 192.168.50.10 connects, if you have a server there. Anything else in the lab is unreachable, and a phone on the Lab SSID can’t reach your laptop.

You put the lab in its own zone and wrote its firewall rules. Next, in Part 5, you’ll give the lab server a name.