From c823c0eb3011bab335b9cf397457a96cdbd3a9a3 Mon Sep 17 00:00:00 2001 From: Alex Jurkiewicz Date: Fri, 3 Apr 2026 15:27:18 +0800 Subject: [PATCH 1/4] feat(aws): restrict security group inbound to current user IP by default Previously all inbound ports were open to 0.0.0.0/0 and ::/0. This adds a --no-restrict-to-my-ip flag (and matching interactive prompt) to control a new restrictToMyIp option, which is enabled by default. When enabled, the provisioner detects the user's current IPv4 and IPv6 addresses before each Pulumi run by making a request to checkip.global.api.aws with a 5-second timeout per address family. The resulting /32 and /128 CIDRs are passed to the Pulumi stack and used as the security group ingress CIDR instead of the open defaults. IPv6 is optional: if the user has no external IPv6 address the timeout fires and IPv6 ingress rules are skipped. IPv4 is required: failure to detect it raises an error with a hint to use --no-restrict-to-my-ip. Co-Authored-By: Claude Sonnet 4.6 --- src/providers/aws/cli.ts | 19 ++++++++++++-- src/providers/aws/provisioner.ts | 44 ++++++++++++++++++++++++++++++-- src/providers/aws/pulumi/main.ts | 17 ++++++++---- src/providers/aws/state.ts | 1 + 4 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/providers/aws/cli.ts b/src/providers/aws/cli.ts index be2cbb5d..a247bec2 100644 --- a/src/providers/aws/cli.ts +++ b/src/providers/aws/cli.ts @@ -35,6 +35,7 @@ export const AwsCreateCliArgsSchema = CreateCliArgsSchema.extend({ baseImageKeepOnDeletion: z.boolean().optional(), dataDiskSnapshot: z.boolean().optional(), deleteInstanceServerOnStop: z.boolean().optional(), + restrictToMyIp: z.boolean().optional(), }) /** @@ -89,6 +90,7 @@ export class AwsInputPrompter extends AbstractInputPrompter { + if (restrictToMyIp !== undefined) { + return restrictToMyIp + } + return await confirm({ + message: 'Restrict inbound traffic to your current IP address?', + default: true, + }) + } + private async instanceType(region: string, useSpot: boolean, instanceType?: string): Promise { if (instanceType) { @@ -325,6 +339,7 @@ export class AwsCliCommandGenerator extends CliCommandGenerator { .option('--region ', 'Region in which to deploy instance') .option('--zone ', 'Availability zone in which to deploy instance') .option('--image-id ', 'Existing AMI ID for instance server. Disk size must be equal or greater than image size.') + .option('--no-restrict-to-my-ip', 'Allow inbound traffic from all IPs instead of restricting to your current IP') .action(async (rawCliArgs: unknown) => { // Parse raw CLI args using Zod schema early to ensure type safety const cliArgs = AwsCreateCliArgsSchema.parse(rawCliArgs) diff --git a/src/providers/aws/provisioner.ts b/src/providers/aws/provisioner.ts index bcf7a3ee..2a93ce1f 100644 --- a/src/providers/aws/provisioner.ts +++ b/src/providers/aws/provisioner.ts @@ -1,3 +1,4 @@ +import * as https from 'https'; import { SshKeyLoader } from '../../tools/ssh'; import { AwsPulumiClient, PulumiStackConfigAws } from './pulumi/main'; import { AwsDataDiskSnapshotPulumiClient, PulumiStackConfigAwsDataDiskSnapshot } from './pulumi/data-volume-snapshot'; @@ -7,6 +8,29 @@ import { AwsClient } from './sdk-client'; import { AwsProvisionInputV1, AwsProvisionOutputV1 } from './state'; import { DATA_DISK_STATE_LIVE, DATA_DISK_STATE_SNAPSHOT } from '../../core/const'; +/** + * Fetch the current external IP address using the specified address family. + * Returns undefined if the request fails or times out (e.g. no IPv6 connectivity). + */ +function fetchCurrentIp(family: 4 | 6, timeoutMs = 5000): Promise { + return new Promise((resolve) => { + const req = https.request({ + hostname: 'checkip.global.api.aws', + path: '/', + method: 'GET', + family, + timeout: timeoutMs, + }, (res) => { + let data = '' + res.on('data', (chunk: string) => { data += chunk }) + res.on('end', () => resolve(data.trim())) + }) + req.on('timeout', () => { req.destroy(); resolve(undefined) }) + req.on('error', () => resolve(undefined)) + req.end() + }) +} + export type AwsProvisionerArgs = InstanceProvisionerArgs export class AwsProvisioner extends AbstractInstanceProvisioner { @@ -92,7 +116,7 @@ export class AwsProvisioner extends AbstractInstanceProvisioner { const sshPublicKeyContent = new SshKeyLoader().loadSshPublicKeyContent(this.args.provisionInput.ssh) + let allowedCidrs: { ipv4: string[], ipv6: string[] } | undefined = undefined + if (this.args.provisionInput.restrictToMyIp) { + const [ipv4, ipv6] = await Promise.all([fetchCurrentIp(4), fetchCurrentIp(6)]) + + if (!ipv4) { + throw new Error("Could not detect current IPv4 address. Check your internet connection, or use --no-restrict-to-my-ip to skip IP restriction.") + } + + this.logger.info(`Detected current IPs for security group: IPv4=${ipv4}${ipv6 ? `, IPv6=${ipv6}` : " (no IPv6 detected)"}`) + allowedCidrs = { + ipv4: [`${ipv4}/32`], + ipv6: ipv6 ? [`${ipv6}/128`] : [], + } + } + return { instanceType: this.args.provisionInput.instanceType, publicIpType: this.args.provisionInput.publicIpType, @@ -156,6 +195,7 @@ export class AwsProvisioner extends AbstractInstanceProvisioner | void> { const publicKeyContent = config.require("publicSshKeyContent"); const useSpot = config.requireBoolean("useSpot"); const ingressPorts = config.requireObject("ingressPorts") + const allowedCidrs = config.getObject<{ ipv4: string[], ipv6: string[] }>("allowedCidrs") const imageId = config.get("imageId") const dataDisk = config.getObject<{ state: "present" | "absent", sizeGb: number, snapshotId?: string }>("dataDisk") const instanceServerState = config.get("instanceServerState") as "present" | "absent" | undefined @@ -368,11 +369,11 @@ async function awsPulumiProgram(): Promise | void> { dataDisk: dataDisk, instanceServerState: instanceServerState, ingressPorts: ingressPorts.map(p => ({ - fromPort: p.port, - toPort: p.port, - protocol: p.protocol, - cidrBlocks: ["0.0.0.0/0"], - ipv6CidrBlocks: ["::/0"] + fromPort: p.port, + toPort: p.port, + protocol: p.protocol, + cidrBlocks: allowedCidrs?.ipv4 ?? ["0.0.0.0/0"], + ipv6CidrBlocks: allowedCidrs?.ipv6 ?? ["::/0"], })) }) @@ -405,6 +406,10 @@ export interface PulumiStackConfigAws { notificationEmail: string }, ingressPorts: SimplePortDefinition[] + allowedCidrs?: { + ipv4: string[] + ipv6: string[] + } } export interface AwsPulumiOutput { @@ -463,6 +468,8 @@ export class AwsPulumiClient extends InstancePulumiClient Date: Fri, 3 Apr 2026 18:04:13 +0800 Subject: [PATCH 2/4] feat: add descriptions to security group ingress rules Add description field to SimplePortDefinition interface and populate descriptions for all Sunshine and Wolf ports. Wire description into AWS security group ingress rule mapping. Co-Authored-By: Claude Sonnet 4.6 --- src/core/const.ts | 74 ++++++++++++++++---------------- src/providers/aws/pulumi/main.ts | 1 + 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/src/core/const.ts b/src/core/const.ts index 02eb81bc..0304cc9c 100644 --- a/src/core/const.ts +++ b/src/core/const.ts @@ -68,6 +68,7 @@ export const CLOUDYPAD_SUNSHINE_IMAGE_REGISTRY = "ghcr.io/pierrebeucher/cloudypa export interface SimplePortDefinition { port: number protocol: string + description?: string } /** @@ -75,33 +76,33 @@ export interface SimplePortDefinition { * See https://games-on-whales.github.io/wolf/stable/user/quickstart.html */ export const CLOUDYPAD_WOLF_PORTS: SimplePortDefinition[] = [ - { port: 22, protocol: 'tcp' }, // SSH - { port: 47984, protocol: 'tcp' }, // HTTPS - { port: 47989, protocol: 'tcp' }, // HTTP - { port: 47999, protocol: 'udp' }, // Control - { port: 48010, protocol: 'tcp' }, // RTSP - { port: 48100, protocol: 'udp' }, // Video (up to 10 users, you can open more ports if needed) - { port: 48101, protocol: 'udp' }, - { port: 48102, protocol: 'udp' }, - { port: 48103, protocol: 'udp' }, - { port: 48104, protocol: 'udp' }, - { port: 48105, protocol: 'udp' }, - { port: 48106, protocol: 'udp' }, - { port: 48107, protocol: 'udp' }, - { port: 48108, protocol: 'udp' }, - { port: 48109, protocol: 'udp' }, - { port: 48110, protocol: 'udp' }, - { port: 48200, protocol: 'udp' }, // Audio (up to 10 users, you can open more ports if needed) - { port: 48201, protocol: 'udp' }, - { port: 48202, protocol: 'udp' }, - { port: 48203, protocol: 'udp' }, - { port: 48204, protocol: 'udp' }, - { port: 48205, protocol: 'udp' }, - { port: 48206, protocol: 'udp' }, - { port: 48207, protocol: 'udp' }, - { port: 48208, protocol: 'udp' }, - { port: 48209, protocol: 'udp' }, - { port: 48210, protocol: 'udp' }, + { port: 22, protocol: 'tcp', description: 'SSH' }, + { port: 47984, protocol: 'tcp', description: 'Wolf HTTPS control' }, + { port: 47989, protocol: 'tcp', description: 'Wolf HTTP control' }, + { port: 47999, protocol: 'udp', description: 'Wolf Moonlight control channel' }, + { port: 48010, protocol: 'tcp', description: 'Wolf RTSP stream setup' }, + { port: 48100, protocol: 'udp', description: 'Wolf video stream (user 1)' }, + { port: 48101, protocol: 'udp', description: 'Wolf video stream (user 2)' }, + { port: 48102, protocol: 'udp', description: 'Wolf video stream (user 3)' }, + { port: 48103, protocol: 'udp', description: 'Wolf video stream (user 4)' }, + { port: 48104, protocol: 'udp', description: 'Wolf video stream (user 5)' }, + { port: 48105, protocol: 'udp', description: 'Wolf video stream (user 6)' }, + { port: 48106, protocol: 'udp', description: 'Wolf video stream (user 7)' }, + { port: 48107, protocol: 'udp', description: 'Wolf video stream (user 8)' }, + { port: 48108, protocol: 'udp', description: 'Wolf video stream (user 9)' }, + { port: 48109, protocol: 'udp', description: 'Wolf video stream (user 10)' }, + { port: 48110, protocol: 'udp', description: 'Wolf video stream (user 11)' }, + { port: 48200, protocol: 'udp', description: 'Wolf audio stream (user 1)' }, + { port: 48201, protocol: 'udp', description: 'Wolf audio stream (user 2)' }, + { port: 48202, protocol: 'udp', description: 'Wolf audio stream (user 3)' }, + { port: 48203, protocol: 'udp', description: 'Wolf audio stream (user 4)' }, + { port: 48204, protocol: 'udp', description: 'Wolf audio stream (user 5)' }, + { port: 48205, protocol: 'udp', description: 'Wolf audio stream (user 6)' }, + { port: 48206, protocol: 'udp', description: 'Wolf audio stream (user 7)' }, + { port: 48207, protocol: 'udp', description: 'Wolf audio stream (user 8)' }, + { port: 48208, protocol: 'udp', description: 'Wolf audio stream (user 9)' }, + { port: 48209, protocol: 'udp', description: 'Wolf audio stream (user 10)' }, + { port: 48210, protocol: 'udp', description: 'Wolf audio stream (user 11)' }, ] /** @@ -111,16 +112,15 @@ export const CLOUDYPAD_WOLF_PORTS: SimplePortDefinition[] = [ * See archive: https://web.archive.org/web/20241228223157/https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/advanced_usage.html#port */ export const CLOUDYPAD_SUNSHINE_PORTS: SimplePortDefinition[] = [ - { port: 22, protocol: 'tcp' }, // SSH - { port: 47984, protocol: 'tcp' }, // HTTPS - { port: 47989, protocol: 'tcp' }, // HTTP - { port: 47990, protocol: 'tcp' }, // Web - { port: 48010, protocol: 'tcp' }, // RTSP - - { port: 47998, protocol: 'udp' }, // Video - { port: 47999, protocol: 'udp' }, // Control - { port: 48000, protocol: 'udp' }, // Audio - { port: 48002, protocol: 'udp' }, // Mic (unused) + { port: 22, protocol: 'tcp', description: 'SSH' }, + { port: 47984, protocol: 'tcp', description: 'Sunshine HTTPS control' }, + { port: 47989, protocol: 'tcp', description: 'Sunshine HTTP control' }, + { port: 47990, protocol: 'tcp', description: 'Sunshine web UI' }, + { port: 48010, protocol: 'tcp', description: 'Sunshine RTSP stream setup' }, + { port: 47998, protocol: 'udp', description: 'Sunshine video stream' }, + { port: 47999, protocol: 'udp', description: 'Sunshine Moonlight control channel' }, + { port: 48000, protocol: 'udp', description: 'Sunshine audio stream' }, + { port: 48002, protocol: 'udp', description: 'Sunshine microphone input (client to server)' }, ] /** diff --git a/src/providers/aws/pulumi/main.ts b/src/providers/aws/pulumi/main.ts index 325c5513..b04c8ac5 100644 --- a/src/providers/aws/pulumi/main.ts +++ b/src/providers/aws/pulumi/main.ts @@ -374,6 +374,7 @@ async function awsPulumiProgram(): Promise | void> { protocol: p.protocol, cidrBlocks: allowedCidrs?.ipv4 ?? ["0.0.0.0/0"], ipv6CidrBlocks: allowedCidrs?.ipv6 ?? ["::/0"], + description: p.description, })) }) From e3466bd1481fe4aa14984e32eb543aa5cb8051d2 Mon Sep 17 00:00:00 2001 From: Alex Jurkiewicz Date: Sun, 5 Apr 2026 10:16:37 +0800 Subject: [PATCH 3/4] feat: add IP detection utilities to src/tools/ip.ts --- src/tools/ip.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/tools/ip.ts diff --git a/src/tools/ip.ts b/src/tools/ip.ts new file mode 100644 index 00000000..41903531 --- /dev/null +++ b/src/tools/ip.ts @@ -0,0 +1,42 @@ +import * as https from 'https' + +/** + * Fetch the current external IP address for the given address family. + * + * Returns undefined if the request fails or times out (e.g. no IPv6 connectivity). + * Uses the AWS checkip endpoint which is reliable and provider-agnostic. + */ +export function fetchCurrentIp(family: 4 | 6, timeoutMs = 5000): Promise { + return new Promise((resolve) => { + const req = https.request({ + hostname: 'checkip.global.api.aws', + path: '/', + method: 'GET', + family, + timeout: timeoutMs, + }, (res) => { + let data = '' + res.on('data', (chunk: string) => { data += chunk }) + res.on('end', () => resolve(data.trim())) + }) + req.on('timeout', () => { req.destroy(); resolve(undefined) }) + req.on('error', () => resolve(undefined)) + req.end() + }) +} + +/** + * Fetch the current external IPv4 and IPv6 addresses and return them as CIDR ranges. + * Throws if the IPv4 address cannot be detected. + * IPv6 is best-effort; an empty array is returned if unavailable. + */ +export async function fetchCurrentIpCidrs(): Promise<{ ipv4: string[], ipv6: string[] }> { + const [ipv4, ipv6] = await Promise.all([fetchCurrentIp(4), fetchCurrentIp(6)]) + if (!ipv4) { + throw new Error('Could not detect current IPv4 address. Check your internet connection.') + } + return { + ipv4: [`${ipv4}/32`], + ipv6: ipv6 ? [`${ipv6}/128`] : [], + } +} From 6e050b49ec0767f4a6f33e84c6b5a6e5a79abd40 Mon Sep 17 00:00:00 2001 From: Alex Jurkiewicz Date: Sun, 5 Apr 2026 10:16:45 +0800 Subject: [PATCH 4/4] =?UTF-8?q?refactor(aws):=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20store=20allowedCidrs=20in=20state,=20resolve=20IPs?= =?UTF-8?q?=20in=20CLI=20layer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace restrictToMyIp boolean flag with allowedCidrs in state schema. Defaults to open access (0.0.0.0/0); stores restricted /32+/128 when IP restriction is enabled. Refreshed on every provision by the provisioner. - Move IP detection to CLI layer (resolveAllowedCidrs): fetches current IP at create time based on --no-restrict-to-my-ip flag (default: restrict). - Provisioner re-fetches IPs on each provision so the security group stays current across create and start flows. Open CIDRs are passed through as-is. --- src/providers/aws/cli.ts | 42 +++++++++++------ src/providers/aws/provisioner.ts | 47 ++++--------------- src/providers/aws/state.ts | 5 +- .../instances/aws-dummy/state.yml | 5 ++ test/unit/providers/aws/cli.spec.ts | 5 +- 5 files changed, 50 insertions(+), 54 deletions(-) diff --git a/src/providers/aws/cli.ts b/src/providers/aws/cli.ts index a247bec2..e429a89a 100644 --- a/src/providers/aws/cli.ts +++ b/src/providers/aws/cli.ts @@ -1,4 +1,5 @@ import { AwsInstanceInput, AwsInstanceStateV1, AwsProvisionInputV1, AwsStateParser } from "./state" +import { fetchCurrentIpCidrs } from '../../tools/ip' import { CommonConfigurationInputV1, CommonInstanceInput } from "../../core/state/state" import { input, select, confirm } from '@inquirer/prompts'; import { AwsClient, EC2_QUOTA_CODE_ALL_G_AND_VT_SPOT_INSTANCES, EC2_QUOTA_CODE_RUNNING_ON_DEMAND_G_AND_VT_INSTANCES, DEFAULT_REGION } from "./sdk-client"; @@ -35,7 +36,7 @@ export const AwsCreateCliArgsSchema = CreateCliArgsSchema.extend({ baseImageKeepOnDeletion: z.boolean().optional(), dataDiskSnapshot: z.boolean().optional(), deleteInstanceServerOnStop: z.boolean().optional(), - restrictToMyIp: z.boolean().optional(), + restrictToMyIp: z.boolean().default(true), }) /** @@ -76,11 +77,17 @@ export const SUPPORTED_INSTANCE_TYPES = [ export class AwsInputPrompter extends AbstractInputPrompter { + // Stashed from buildProvisionerInputFromCliArgs for use in resolveAllowedCidrs. + // restrictToMyIp is a CLI-only concept; state stores resolved allowedCidrs instead. + // Defaults to false (no restriction); set to true by Commander's --no-restrict-to-my-ip default. + private _cliRestrictToMyIp = false + constructor(args: AbstractInputPrompterArgs){ super(args) } buildProvisionerInputFromCliArgs(cliArgs: AwsCreateCliArgs): PartialDeep { + this._cliRestrictToMyIp = cliArgs.restrictToMyIp return { provision: { @@ -90,13 +97,12 @@ export class AwsInputPrompter extends AbstractInputPrompter { - if (restrictToMyIp !== undefined) { - return restrictToMyIp + /** + * Resolve allowed CIDRs for security group ingress based on CLI flags. + * + * - If --no-restrict-to-my-ip was passed, use open CIDRs (no restriction). + * - Otherwise (default), fetch and return the user's current IP as restricted CIDRs. + */ + private async resolveAllowedCidrs(): Promise<{ ipv4: string[], ipv6: string[] }> { + if (!this._cliRestrictToMyIp) { + return { ipv4: ['0.0.0.0/0'], ipv6: ['::/0'] } } - return await confirm({ - message: 'Restrict inbound traffic to your current IP address?', - default: true, - }) + + const cidrs = await fetchCurrentIpCidrs() + this.logger.info( + `Detected current IPs: IPv4=${cidrs.ipv4[0]}${cidrs.ipv6[0] ? `, IPv6=${cidrs.ipv6[0]}` : ' (no IPv6 detected)'}` + ) + return cidrs } private async instanceType(region: string, useSpot: boolean, instanceType?: string): Promise { diff --git a/src/providers/aws/provisioner.ts b/src/providers/aws/provisioner.ts index 2a93ce1f..bad4a46e 100644 --- a/src/providers/aws/provisioner.ts +++ b/src/providers/aws/provisioner.ts @@ -1,36 +1,13 @@ -import * as https from 'https'; import { SshKeyLoader } from '../../tools/ssh'; import { AwsPulumiClient, PulumiStackConfigAws } from './pulumi/main'; import { AwsDataDiskSnapshotPulumiClient, PulumiStackConfigAwsDataDiskSnapshot } from './pulumi/data-volume-snapshot'; import { AwsBaseImagePulumiClient, PulumiStackConfigAwsBaseImage } from './pulumi/base-image-snapshot'; import { AbstractInstanceProvisioner, InstanceProvisionerArgs, ProvisionerActionOptions } from '../../core/provisioner'; import { AwsClient } from './sdk-client'; +import { fetchCurrentIpCidrs } from '../../tools/ip'; import { AwsProvisionInputV1, AwsProvisionOutputV1 } from './state'; import { DATA_DISK_STATE_LIVE, DATA_DISK_STATE_SNAPSHOT } from '../../core/const'; -/** - * Fetch the current external IP address using the specified address family. - * Returns undefined if the request fails or times out (e.g. no IPv6 connectivity). - */ -function fetchCurrentIp(family: 4 | 6, timeoutMs = 5000): Promise { - return new Promise((resolve) => { - const req = https.request({ - hostname: 'checkip.global.api.aws', - path: '/', - method: 'GET', - family, - timeout: timeoutMs, - }, (res) => { - let data = '' - res.on('data', (chunk: string) => { data += chunk }) - res.on('end', () => resolve(data.trim())) - }) - req.on('timeout', () => { req.destroy(); resolve(undefined) }) - req.on('error', () => resolve(undefined)) - req.end() - }) -} - export type AwsProvisionerArgs = InstanceProvisionerArgs export class AwsProvisioner extends AbstractInstanceProvisioner { @@ -170,19 +147,15 @@ export class AwsProvisioner extends AbstractInstanceProvisioner { const sshPublicKeyContent = new SshKeyLoader().loadSshPublicKeyContent(this.args.provisionInput.ssh) - let allowedCidrs: { ipv4: string[], ipv6: string[] } | undefined = undefined - if (this.args.provisionInput.restrictToMyIp) { - const [ipv4, ipv6] = await Promise.all([fetchCurrentIp(4), fetchCurrentIp(6)]) - - if (!ipv4) { - throw new Error("Could not detect current IPv4 address. Check your internet connection, or use --no-restrict-to-my-ip to skip IP restriction.") - } - - this.logger.info(`Detected current IPs for security group: IPv4=${ipv4}${ipv6 ? `, IPv6=${ipv6}` : " (no IPv6 detected)"}`) - allowedCidrs = { - ipv4: [`${ipv4}/32`], - ipv6: ipv6 ? [`${ipv6}/128`] : [], - } + // If the user chose open access (0.0.0.0/0), preserve that choice as-is. + // If the user chose IP restriction, re-fetch their current IP on every provision + // so the security group stays current across create and start flows. + let allowedCidrs = this.args.provisionInput.allowedCidrs + if (allowedCidrs.ipv4[0] !== '0.0.0.0/0') { + allowedCidrs = await fetchCurrentIpCidrs() + this.logger.info( + `Refreshed IPs for security group: IPv4=${allowedCidrs.ipv4[0]}${allowedCidrs.ipv6[0] ? `, IPv6=${allowedCidrs.ipv6[0]}` : ' (no IPv6 detected)'}` + ) } return { diff --git a/src/providers/aws/state.ts b/src/providers/aws/state.ts index 138971da..479de8cd 100644 --- a/src/providers/aws/state.ts +++ b/src/providers/aws/state.ts @@ -17,7 +17,10 @@ const AwsProvisionInputV1Schema = CommonProvisionInputV1Schema.extend({ region: z.string().describe("AWS region"), zone: z.string().optional().describe("AWS availability zone"), useSpot: z.boolean().describe("Whether to use spot instances"), - restrictToMyIp: z.boolean().default(true).describe("Restrict security group inbound rules to the current user's IP addresses"), + allowedCidrs: z.object({ + ipv4: z.array(z.string()), + ipv6: z.array(z.string()), + }).default({ ipv4: ['0.0.0.0/0'], ipv6: ['::/0'] }).describe("Allowed CIDRs for security group ingress. Defaults to open access; set to specific ranges to restrict inbound traffic to those IPs, refreshed on every provision."), costAlert: z.object({ limit: z.number().describe("Cost alert limit (USD)"), notificationEmail: z.string().describe("Cost alert notification email"), diff --git a/test/resources/states/v1-root-data-dir/instances/aws-dummy/state.yml b/test/resources/states/v1-root-data-dir/instances/aws-dummy/state.yml index 1246f3ef..7ed08357 100644 --- a/test/resources/states/v1-root-data-dir/instances/aws-dummy/state.yml +++ b/test/resources/states/v1-root-data-dir/instances/aws-dummy/state.yml @@ -11,6 +11,11 @@ provision: publicIpType: static region: eu-central-1 useSpot: true + allowedCidrs: + ipv4: + - 0.0.0.0/0 + ipv6: + - ::/0 ssh: user: ubuntu privateKeyContentBase64: ZHVtbXkta2V5 diff --git a/test/unit/providers/aws/cli.spec.ts b/test/unit/providers/aws/cli.spec.ts index 9e193776..0fd4cef6 100644 --- a/test/unit/providers/aws/cli.spec.ts +++ b/test/unit/providers/aws/cli.spec.ts @@ -34,6 +34,7 @@ describe('AWS input prompter', () => { enable: true, }, deleteInstanceServerOnStop: true, + allowedCidrs: { ipv4: ['0.0.0.0/0'], ipv6: ['::/0'] }, }, configuration: { ...DEFAULT_COMMON_INPUT.configuration @@ -69,8 +70,8 @@ describe('AWS input prompter', () => { const expected: PartialDeep = { ...TEST_INPUT, provision: { - // publicIpType is not set via CLI - ...lodash.omit(TEST_INPUT.provision, "publicIpType"), + // publicIpType and allowedCidrs are not set via CLI args — resolved at prompt time + ...lodash.omit(TEST_INPUT.provision, "publicIpType", "allowedCidrs"), ssh: lodash.omit(TEST_INPUT.provision.ssh, "user"), costAlert: { limit: 999,