alien-core 3.3.57

Deploy software into your customers' cloud accounts and keep it fully managed
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
//! AWS sandbox connector documents and gateway network boundaries:
//! the operator role Lambda assumes to place the connector's network interfaces, and the
//! `AWS::Lambda::NetworkConnector` those interfaces belong to.
//!
//! The CloudFormation and Terraform emitters write the same objects as template expressions; the
//! generator parity tests fail if the resolved forms here ever disagree with them.

use std::fmt;

use serde_json::{json, Value};

use crate::{
    setup_resource_tags, Network, NetworkSettings, ResourceEntry, Sandbox, SandboxEgress, Stack,
};

/// Reserved resource id of the deployment's sandbox egress gateway Container.
pub const SANDBOX_EGRESS_GATEWAY_ID: &str = "sandbox-egress-gateway";

/// The gateway's replica secret, named `{resource prefix}-sandbox-egress-replica-secret` in
/// AWS Secrets Manager, where setup generates it. On Azure and Kubernetes it is this entry of
/// the deployment secrets vault, written once and never read back by its writer. Only the
/// gateway reads it.
pub const SANDBOX_EGRESS_REPLICA_SECRET: &str = "sandbox-egress-replica-secret";

/// Gateway images pinned before gateways read their replica secret from a source. A gateway on
/// one of these needs the secret embedded in its configuration; every later image reads the
/// source. The list never grows.
pub const EMBEDDED_REPLICA_SECRET_GATEWAY_DIGESTS: &[&str] = &[
    "sha256:dbac794afe2185a8ce025d92ec05a5878d6367ae9380eccc2c3dfc4d55f1c59c",
    "sha256:1dc14128e34426b84c7a02228a6de790ea092b156776c3f2688b9843b26fc924",
    "sha256:7a3a66b0894c05fca445cbf5fd71e351cd8a9a98c40744acad4496b59574bdbd",
    "sha256:f0e47d06eea327bc27fdc1238dae6a8938572db0c1d139495d00dd5623b657dd",
    "sha256:b252a1b423b95385e5df0c57a37daae6937daa274e3c7783c71704584b18483d",
];

/// Gateway images pinned after gateways read a replica secret source but before they read their
/// configuration and replica secret from mounted files. The list never grows.
pub const NO_CONFIG_FILE_GATEWAY_DIGESTS: &[&str] =
    &["sha256:8538880f99141888e54bedc7d290194dfee5bfe11910266b309dde53de2601b0"];

fn is_pinned_to(image: &str, digests: &[&str]) -> bool {
    digests
        .iter()
        .any(|digest| image.ends_with(&format!("@{digest}")))
}

/// Whether a gateway running `image` reads its replica secret from a source.
pub fn gateway_image_reads_replica_secret_source(image: &str) -> bool {
    !is_pinned_to(image, EMBEDDED_REPLICA_SECRET_GATEWAY_DIGESTS)
}

/// Whether a gateway running `image` reads its configuration from a file it refreshes and its
/// replica secret from a file. Kubernetes mounts both from Secrets the gateway's managing
/// identity writes and never reads.
pub fn gateway_image_reads_config_files(image: &str) -> bool {
    gateway_image_reads_replica_secret_source(image)
        && !is_pinned_to(image, NO_CONFIG_FILE_GATEWAY_DIGESTS)
}

/// Whether preflight installed the managed gateway for this deployment.
pub fn has_sandbox_gateway(stack: &Stack) -> bool {
    stack
        .resources
        .get(SANDBOX_EGRESS_GATEWAY_ID)
        .is_some_and(|entry| entry.config.downcast_ref::<crate::Container>().is_some())
}

/// The stack's first Sandbox with internet egress, if any. Such a sandbox reserves
/// [`SANDBOX_EGRESS_GATEWAY_ID`] for the gateway Alien manages.
fn first_internet_sandbox(stack: &Stack) -> Option<&String> {
    stack
        .resources()
        .find(|(_, entry)| {
            entry
                .config
                .downcast_ref::<Sandbox>()
                .is_some_and(|sandbox| matches!(sandbox.egress, SandboxEgress::Internet))
        })
        .map(|(id, _)| id)
}

/// Whether the stack has a Sandbox with internet egress.
pub fn has_internet_sandbox(stack: &Stack) -> bool {
    first_internet_sandbox(stack).is_some()
}

/// Whether `resource_id` is the egress gateway Alien manages for this stack, not an
/// application resource.
///
/// The id alone doesn't decide it: an application without an internet sandbox may name its
/// own resource `sandbox-egress-gateway`. With one, the id is reserved for the gateway
/// preparation installs, and an application resource that uses it is refused.
pub fn is_managed_gateway(stack: &Stack, resource_id: &str) -> bool {
    resource_id == SANDBOX_EGRESS_GATEWAY_ID && has_internet_sandbox(stack)
}

/// An AWS internet sandbox's connector routes through the gateway, so a setup surface that
/// creates the connector must also create the gateway's subnets and inputs. Refuses a stack whose
/// internet sandbox has no gateway rather than rendering references to undeclared ones.
pub fn require_internet_sandbox_gateway(stack: &Stack) -> crate::Result<()> {
    if has_sandbox_gateway(stack) {
        return Ok(());
    }
    match first_internet_sandbox(stack) {
        Some(id) => Err(alien_error::AlienError::new(
            crate::ErrorData::SandboxInternetGatewayMissing {
                sandbox_id: id.clone(),
            },
        )),
        None => Ok(()),
    }
}

/// A setup-owned sandbox initializes the shared key before runtime work can start.
/// Within the same lifecycle, resource-id order provides one deterministic writer.
pub fn sandbox_egress_key_owner(stack: &Stack) -> Option<&str> {
    stack
        .resources()
        .filter(|(_, entry)| {
            entry
                .config
                .downcast_ref::<Sandbox>()
                .is_some_and(|sandbox| matches!(sandbox.egress, SandboxEgress::Internet))
        })
        .min_by_key(|(id, entry)| (entry.lifecycle != crate::ResourceLifecycle::Frozen, *id))
        .map(|(id, _)| id.as_str())
}

/// Closed sandboxes retain their denying connector. Only the new internet
/// declaration opts into the gateway, including in stacks with legacy sandboxes.
pub fn sandbox_needs_connector(stack: &Stack, sandbox: &Sandbox) -> bool {
    sandbox_plans_connector(sandbox)
        && (sandbox.egress != SandboxEgress::Internet || has_sandbox_gateway(stack))
}

/// Whether deployment must prepare an external connector for this declaration.
/// Build and compile-time checks run before the managed internet gateway is added.
/// Deployment prerequisites still refuse a target that cannot host that gateway.
pub fn sandbox_plans_connector(sandbox: &Sandbox) -> bool {
    matches!(
        sandbox.cloud_egress(),
        SandboxEgress::None | SandboxEgress::AllowDomains { .. }
    ) || sandbox.egress == SandboxEgress::Internet
}

/// Inspected builds opt into OCI USER startup after Run. Saved declarations without
/// image metadata retain their released startup behavior and command identity.
pub fn aws_sandbox_agent_environment(
    stack: &Stack,
    sandbox: &Sandbox,
) -> std::collections::BTreeMap<String, String> {
    let mut environment = sandbox.supervisor_environment();
    if sandbox.image_user.is_some() {
        environment.insert("ALIEN_SANDBOX_IMAGE_COMMAND".into(), "true".into());
        environment.insert("ALIEN_SANDBOX_EGRESS_RUN_HOOK".into(), "true".into());
        if sandbox.privileged_supervisor.is_none() && sandbox.egress == SandboxEgress::None {
            environment.insert("ALIEN_SANDBOX_EGRESS".into(), "\"none\"".into());
        }
    }
    if matches!(sandbox.egress, SandboxEgress::Internet) && has_sandbox_gateway(stack) {
        environment.insert(
            "ALIEN_SANDBOX_EGRESS".into(),
            serde_json::to_string(&sandbox.egress).expect("egress serializes"),
        );
        // The platform must complete its bootstrap before the guest-wide firewall
        // is installed. The authenticated run hook installs it before user code.
        environment.insert("ALIEN_SANDBOX_EGRESS_RUN_HOOK".into(), "true".into());
    }
    environment
}

/// The name of the operator role's inline policy.
pub const SANDBOX_EGRESS_POLICY_NAME: &str = "sandbox-egress-connector";

/// The registry type Cloud Control creates the connector as.
pub const NETWORK_CONNECTOR_TYPE_NAME: &str = "AWS::Lambda::NetworkConnector";

/// The one destination the deny security group permits, which reaches nothing.
pub const LOOPBACK_ONLY_CIDR: &str = "127.0.0.1/32";

/// The name of both the operator role and the deny security group. Each lives in its own
/// namespace, and a retry finds either again by this name. Never clamped, because
/// `SandboxBuildRoleNameCheck` budgets every deny sandbox's id for it.
pub fn sandbox_egress_name(resource_prefix: &str, sandbox_id: &str) -> String {
    format!("{resource_prefix}-{sandbox_id}-egress")
}

/// The connector's name, unique per account and Region, which is how a retry finds it again.
pub fn sandbox_egress_connector_name(resource_prefix: &str, sandbox_id: &str) -> String {
    format!("{resource_prefix}-{sandbox_id}")
}

/// Lambda may assume the operator role. No `aws:SourceAccount` condition: the emitters write none.
pub fn sandbox_egress_operator_trust_policy() -> Value {
    json!({
        "Version": "2012-10-17",
        "Statement": [{
            "Effect": "Allow",
            "Principal": { "Service": "lambda.amazonaws.com" },
            "Action": "sts:AssumeRole"
        }]
    })
}

/// Lets Lambda create the connector's network interfaces and tag each one only as it creates it.
/// Unlike AWS's `AWSLambdaNetworkConnectorOperatorPolicy` it is pinned to this account and Region,
/// and puts no `aws:TagKeys` condition on the interface it creates.
pub fn sandbox_egress_operator_policy(partition: &str, account_id: &str, region: &str) -> Value {
    let arn = |kind: &str| format!("arn:{partition}:ec2:{region}:{account_id}:{kind}/*");
    json!({
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "CreateENI",
                "Effect": "Allow",
                "Action": "ec2:CreateNetworkInterface",
                "Resource": [arn("network-interface"), arn("subnet"), arn("security-group")]
            },
            {
                "Sid": "TagENI",
                "Effect": "Allow",
                "Action": "ec2:CreateTags",
                "Resource": arn("network-interface"),
                "Condition": {
                    "StringEquals": {
                        "ec2:CreateAction": "CreateNetworkInterface",
                        "ec2:ManagedResourceOperator": "network-connectors.lambda.amazonaws.com"
                    }
                }
            }
        ]
    })
}

/// The connector a deny sandbox's sessions start with, as Cloud Control's `DesiredState`.
///
/// `NetworkProtocol` is optional in the schema and refused when absent. IPv4 because the deny
/// group matches IPv4 CIDRs only, so a v6 path would sit outside it.
#[derive(Debug, Clone, bon::Builder)]
pub struct SandboxEgressConnector<'a> {
    resource_prefix: &'a str,
    sandbox_id: &'a str,
    operator_role_arn: &'a str,
    private_subnet_ids: &'a [String],
    security_group_id: &'a str,
}

impl SandboxEgressConnector<'_> {
    pub fn desired_state(&self) -> Value {
        let tags: Vec<Value> = setup_resource_tags(
            self.resource_prefix,
            self.sandbox_id,
            Sandbox::RESOURCE_TYPE.as_ref(),
        )
        .into_iter()
        .map(|(key, value)| json!({ "Key": key, "Value": value }))
        .collect();
        json!({
            "Name": sandbox_egress_connector_name(self.resource_prefix, self.sandbox_id),
            "OperatorRole": self.operator_role_arn,
            "Configuration": {
                "VpcEgressConfiguration": {
                    "AssociatedComputeResourceTypes": ["MicroVm"],
                    "NetworkProtocol": "IPv4",
                    "SubnetIds": self.private_subnet_ids,
                    "SecurityGroupIds": [self.security_group_id]
                }
            },
            "Tags": tags
        })
    }
}

/// Where a VPC with private subnets comes from, which is all that separates how the emitters
/// render the connector's subnets.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SandboxEgressVpc {
    /// The network is `create`: setup builds the VPC and its private subnets.
    Created,
    /// The network is `byo-vpc-aws`: the customer names the VPC and its private subnets.
    BroughtByCustomer,
}

/// The network every sandbox connector attaches to.
#[derive(Debug, Clone, Copy)]
pub struct SandboxEgressNetwork<'a> {
    /// The network's resource id in the stack.
    pub id: &'a str,
    /// The network's stack entry, which carries its lifecycle.
    pub entry: &'a ResourceEntry,
    pub vpc: SandboxEgressVpc,
}

/// Why an AWS sandbox's egress cannot be built from this stack.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SandboxEgressRefusal {
    /// The stack declares no network for the connector to attach to.
    NoNetwork,
    /// The network is the account's default VPC, which has no private subnets.
    DefaultVpc,
    /// The network's settings are for GCP or Azure.
    OtherCloudNetwork,
}

impl fmt::Display for SandboxEgressRefusal {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("an AWS sandbox routes session traffic through a VPC egress connector")?;
        f.write_str(match self {
            Self::NoNetwork => ", and this stack declares no network for it to attach to",
            Self::DefaultVpc => {
                ", which needs private subnets, and the account's default VPC has only public \
                 subnets. Set the network to create or byo-vpc-aws"
            }
            Self::OtherCloudNetwork => ", and this stack's network settings are for another cloud",
        })
    }
}

/// Resolve the setup-owned network for a sandbox that needs an isolated connector.
pub fn sandbox_egress_network(
    stack: &Stack,
) -> Result<SandboxEgressNetwork<'_>, SandboxEgressRefusal> {
    let (id, entry, network) = stack
        .resources()
        .find_map(|(id, entry)| Some((id, entry, entry.config.downcast_ref::<Network>()?)))
        .ok_or(SandboxEgressRefusal::NoNetwork)?;
    let vpc = match &network.settings {
        NetworkSettings::Create { .. } => SandboxEgressVpc::Created,
        NetworkSettings::ByoVpcAws { .. } => SandboxEgressVpc::BroughtByCustomer,
        NetworkSettings::UseDefault => return Err(SandboxEgressRefusal::DefaultVpc),
        NetworkSettings::ByoVpcGcp { .. } | NetworkSettings::ByoVnetAzure { .. } => {
            return Err(SandboxEgressRefusal::OtherCloudNetwork)
        }
    };
    Ok(SandboxEgressNetwork { id, entry, vpc })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{ResourceLifecycle, SandboxCode, SandboxEgress, SandboxLifecyclePolicy};
    use SandboxEgressRefusal::{DefaultVpc, NoNetwork, OtherCloudNetwork};
    use SandboxEgressVpc::{BroughtByCustomer, Created};

    fn outcome(
        egress: SandboxEgress,
        network: Option<NetworkSettings>,
    ) -> Result<SandboxEgressVpc, SandboxEgressRefusal> {
        let sandbox = Sandbox::new("agents".to_string())
            .code(SandboxCode::Image {
                image: "s3://acme/agents/bundle.zip".to_string(),
                bundled_from: None,
            })
            .egress(egress.clone())
            .lifecycle(SandboxLifecyclePolicy {
                max_lifetime_seconds: None,
                idle_pause_seconds: None,
            })
            .build();
        let mut stack = Stack::new("acme".to_string()).add(sandbox, ResourceLifecycle::Frozen);
        if let Some(settings) = network {
            stack = stack.add(
                Network::new("net".to_string()).settings(settings).build(),
                ResourceLifecycle::Frozen,
            );
        }
        let stack = stack.build();
        let found = sandbox_egress_network(&stack)?;
        assert_eq!(found.id, "net");
        Ok(found.vpc)
    }

    #[test]
    fn a_deny_sandbox_attaches_to_the_stacks_first_network() {
        let create = || NetworkSettings::Create {
            cidr: None,
            availability_zones: 2,
        };
        let byo_aws = || NetworkSettings::ByoVpcAws {
            vpc_id: "vpc-1".to_string(),
            public_subnet_ids: vec!["subnet-pub".to_string()],
            private_subnet_ids: vec!["subnet-priv".to_string()],
            security_group_ids: vec![],
        };
        for (first, second, expected) in [
            (create(), byo_aws(), Created),
            (byo_aws(), create(), BroughtByCustomer),
        ] {
            let stack = Stack::new("acme".to_string())
                .add(
                    Network::new("first".to_string()).settings(first).build(),
                    ResourceLifecycle::Frozen,
                )
                .add(
                    Network::new("second".to_string()).settings(second).build(),
                    ResourceLifecycle::Frozen,
                )
                .build();
            let found =
                sandbox_egress_network(&stack).expect("a created or brought VPC is attachable");
            assert_eq!((found.id, found.vpc), ("first", expected));
        }
    }

    #[test]
    fn the_operator_role_tags_only_the_interface_it_is_creating() {
        let policy = sandbox_egress_operator_policy("aws", "123456789012", "us-east-1");
        let tagging = policy["Statement"]
            .as_array()
            .expect("statements")
            .iter()
            .find(|statement| statement["Action"] == "ec2:CreateTags")
            .expect("the role tags the interfaces it creates");
        assert_eq!(
            tagging["Condition"],
            json!({
                "StringEquals": {
                    "ec2:CreateAction": "CreateNetworkInterface",
                    "ec2:ManagedResourceOperator": "network-connectors.lambda.amazonaws.com"
                }
            })
        );
    }

    #[test]
    fn every_egress_mode_and_network_setting_resolves_to_one_outcome() {
        let create = || NetworkSettings::Create {
            cidr: None,
            availability_zones: 2,
        };
        let byo_aws = || NetworkSettings::ByoVpcAws {
            vpc_id: "vpc-1".to_string(),
            public_subnet_ids: vec!["subnet-pub".to_string()],
            private_subnet_ids: vec!["subnet-priv".to_string()],
            security_group_ids: vec![],
        };
        let byo_gcp = || NetworkSettings::ByoVpcGcp {
            network_name: "vpc".to_string(),
            subnet_name: "subnet".to_string(),
            region: "us-central1".to_string(),
        };

        let cases = [
            (SandboxEgress::Internet, None, Err(NoNetwork)),
            (
                SandboxEgress::Internet,
                Some(NetworkSettings::UseDefault),
                Err(DefaultVpc),
            ),
            (
                SandboxEgress::Internet,
                Some(byo_gcp()),
                Err(OtherCloudNetwork),
            ),
            (SandboxEgress::None, None, Err(NoNetwork)),
            (SandboxEgress::None, Some(create()), Ok(Created)),
            (SandboxEgress::None, Some(byo_aws()), Ok(BroughtByCustomer)),
            (
                SandboxEgress::None,
                Some(NetworkSettings::UseDefault),
                Err(DefaultVpc),
            ),
            (SandboxEgress::None, Some(byo_gcp()), Err(OtherCloudNetwork)),
        ];
        for (egress, network, expected) in cases {
            let label = format!("{egress:?} on {network:?}");
            assert_eq!(outcome(egress, network), expected, "{label}");
        }
    }
}