Skip to main content

Deploying on AWS

AWS runs Polaris on AMD SEV-SNP confidential EC2 instances. Read Deploying Polaris first for the choices common to every cloud; this page covers what is specific to AWS.

A deployment is two confidential instances in one VPC: the CVM, which runs your workload with the Polaris proxy as a sidecar and the sentinel beside it, and the PM, which runs the Policy Manager behind its own proxy. Both run under SEV-SNP, because the CVM's proxy attests the PM before it trusts a verdict from it.

Regions​

SEV-SNP launches work in us-east-2 and eu-west-1, and nowhere else. The config refuses any other region.

Do not pick a region from the instance-type catalogue. describe-instance-types --filters processor-info.supported-features=amd-sev-snp reports SNP-capable types in regions that then refuse the launch with UnsupportedOperation: AMD SEV-SNP is not supported in this Region, because that filter describes the instance type and not regional availability.

Before you start​

Four things you provide.

A KMS key​

Make this once and keep it. It is the key your workload's data is encrypted under, so it has to outlive any deployment that uses it.

aws kms create-key --region us-east-2 \
--description 'Polaris workload data key' \
--query KeyMetadata.Arn --output text
aws kms create-alias --region us-east-2 \
--alias-name alias/polaris --target-key-id <key id>

The Terraform module deliberately creates no key. The Policy Manager grants kms:Decrypt and kms:GenerateDataKey on it to the CVM's role as the attestation verdict changes, so a key the module owned would be scheduled for deletion by terraform destroy, and KMS deletion cannot be undone once the waiting period elapses. A teardown would take the means of decrypting your data with it.

It has to be a customer-managed key, enabled, for ENCRYPT_DECRYPT, in the same region as the deployment. An AWS-managed key takes no grants from another principal, and the Policy Manager creates grants in its own region. deploy.sh checks all four before it creates anything and names which one is wrong.

An AMI​

Instances boot from the packer-baked polaris-cvm-aws image, built by infrastructure/packer/polaris-cvm-aws.pkr.hcl. A stock Ubuntu AMI does not work: the CVM has deny-by-default egress, so a boot path that apt-installs Docker hangs and the instance never comes up. Docker, the AWS CLI, jq, auditd and the IMA boot flags are baked in, and time sync points at the link-local Amazon Time Sync service, which works with no egress.

Leave ami_id unset and deploy.sh selects the newest AMI in the region tagged Family=polaris-cvm-aws, which is the same behaviour as the image family on GCP. Set it to pin an exact image for a reproducible deployment.

The image does not update itself once built. See Patching for why, and for how to take a security fix.

A key pair​

A standard EC2 key pair. Its name goes in key_pair_name and the private key is what reaches the instances.

Credentials​

An account that can create EC2 instances, a VPC with its endpoints, KMS grants, Secrets Manager secrets, and the IAM roles the components use.

Launch with AMD SEV-SNP switched on

On AWS, confidential protection is a per-instance launch setting. An EC2 instance runs as a confidential VM only when AMD SEV-SNP is enabled at launch. Leave it off and the instance boots as an ordinary VM, with no memory encryption and no SEV-SNP attestation report, and AWS raises no error. The workload still runs, so the downgrade is easy to miss.

The Polaris Terraform module and the marketplace listing set this for you. If you launch the Polaris AMI any other way, from the console, the CLI, or your own automation, you have to enable AMD SEV-SNP on the instance yourself. The safe way to confirm it is to verify attestation against the deployment before sending real traffic: a non-confidential instance cannot produce a valid report, so the check fails cleanly. See Verifying a Deployment.

TEE options​

ChoiceHardwareInstance family
AMD SEV-SNPAMD EPYCm6a, c6a, r6a

SEV-SNP stops partway up each family, so the config allows only the shapes that offer it. cvm_instance_type defaults to m6a.xlarge and pm_instance_type to m6a.large.

AWS exposes AMD SEV-SNP only. Intel TDX and confidential GPUs are not available on AWS confidential instances; see the Google Cloud or Microsoft Azure pages for those.

Getting in​

Set ssh_allowed_cidrs or you cannot reach the deployment at all. Empty, which is the default, opens port 22 nowhere, and on AWS SSH is the only way in:

  • The PM has no public address.
  • The SSM agent is in the base image but can never register: Session Manager needs the ssm, ssmmessages and ec2messages interface endpoints, the VPC has none of them, and the CVM's egress is closed. Polaris disables the agent for that reason, since an unregistered AWS-managed agent with remote-execution capability inside a confidential VM buys nothing.
  • The management passthrough on the CVM's public endpoint is served only to the CVM.

Setting it also opens the CVM-to-PM hop, which is how status and promote reach the Policy Manager. ssh_private_key names the key for key_pair_name, defaulting to ~/.ssh/<key_pair_name>.pem.

That hop is authenticated in both directions. The key pair proves you to the server, and deploy pins each guest's SSH host keys by reading them from its EC2 serial console, which AWS serves under your own credentials rather than over the network path being checked. Both legs then run with host-key checking on, against a known_hosts written beside the Terraform state.

Each key is filed under the instance id as well as the address, and the hop looks it up by instance id. That matters because the CVM has no Elastic IP: stopping and starting it hands it a new public address, and a pin tied to the old one would refuse a host that had not changed. Running ssh by hand against the same file still works, since the address is on the line too.

Two consequences worth knowing. A deployment with no pinned keys refuses to connect rather than falling back to an unchecked hop, and the fix is to run deploy, which rewrites the file. And if a guest's host keys ever change without a deploy, which should not happen because the keys live on disk and survive a reboot, the hop fails closed and says so rather than warning and continuing.

Running it​

./scripts/deploy.sh deploy --config my-aws.yaml
./scripts/deploy.sh status --config my-aws.yaml
./scripts/deploy.sh promote --layers l2 --mode enforce --config my-aws.yaml
./scripts/deploy.sh destroy --config my-aws.yaml

status lists the deployment's instances and names the endpoint. promote moves a layer's enforcement dial by reading the baseline the Policy Manager holds, editing it and putting it back, so a converged deployment keeps what it learned instead of being redeployed.

The workload is normally a prebuilt image in ECR. A registry outside the deployment region needs its address range in allowed_egress_cidrs, or the boot-time pull hangs against the deny-by-default egress.

The Policy Manager's identity​

The CVM's proxy attests the PM's SEV-SNP RA-TLS identity before it trusts anything from it, and enforces a launch measurement once it has one.

Left unset, the proxy learns the measurement from the first report that passes the AMD chain and the REPORT_DATA binding, and enforces it from then on. Set pm_measurement_hex and the pin is written before the proxy starts, with POLARIS_PM_SNP_BASELINE_REQUIRED beside it, so a proxy with no usable pin refuses to start rather than learning one.

What that pin is worth on EC2. The SNP measurement there covers AWS's firmware and the VMSAs, not the guest kernel, the initrd, the kernel command line or anything cloud-init does. So it does not identify a particular policy manager: it says "an SNP guest with this vCPU count on this AWS firmware", which every instance of that shape in every AWS account matches. What it buys is a refusal of a differently shaped guest, and a proxy that will not run with no pin at all. Establishing the software identity of a policy manager on AWS is an open problem, tracked in F0X-538.

Computing the value ahead of a boot is possible but needs AWS's exact firmware build, which a deployment does not have (aws/uefi#19, with sev-snp-measure v0.0.13 or later). So in practice the first deployment runs unpinned and deploy.sh prints the value at the end of the run, to be committed and deployed again. An AWS firmware rollout moves it, and with a pin in place the next PM restart onto new firmware is a proxy refusing its own policy manager until someone recaptures.

Node identity​

The Policy Manager names each AWS node from its EC2 instance identity document, as aws:<account>/<instance-id>. cloud-init fetches the document and its signature on the host at boot and writes them where the proxy reads them; the proxy cannot fetch them itself, because the CVM pins the IMDS hop limit to 1 so that no container can reach the metadata service.

The PM verifies the signature against AWS's published certificate for the region, which the module ships and hands it. Unset or wrong, a node gets no registry entry and no node certificate rather than an identity nobody checked.

Two limits worth knowing. The document is not bound to the report that carries it, so an AWS node_id means "a genuine AWS SNP guest presented this signed document" rather than "this guest is that instance"; one node, one attested key limits the damage. And the certificate for the endpoint in use carries a 1024-bit RSA key, which is the weakest link in that chain. Both are tracked, in F0X-538 and F0X-539.

The Policy Manager's state​

/var/lib/polaris-pm on the PM instance is a separate encrypted EBS volume, not a directory on the boot disk. The PM keeps its incident records and its issuer key set there, including node-ca, and a root volume is deleted on termination, so replacing the instance would otherwise regenerate node-ca and invalidate every node certificate the deployment had issued.

An AMI bump replaces the instance, so does a change to any launch-time field, and so does a terminated instance coming back. A stop/start keeps the root volume and is none of these. terraform destroy deletes the volume; terraform output pm_state_volume_id names it for a snapshot first.

If the volume cannot be mounted the PM still starts, on the root volume, and says so on the serial console. That is a working deployment whose identity will not survive the next instance replacement.

Key management​

The Policy Manager gates your KMS key with grants that follow the attestation verdict: a grant stands while appraisal passes and is retired when it fails, and expired grants are retired by the dead man's switch. The key never leaves your account, and enforcement on AWS is therefore a real key revocation rather than a refused connection, which means you can confirm it from outside Polaris by asking KMS whether the grant is there.

Patching​

The image does not patch itself, and that is deliberate. unattended-upgrades is removed from the baked AMI and apt-daily.timer, apt-daily-upgrade.timer and unattended-upgrades.service are masked, on both guests. Nothing installs packages after the image is built.

The reason is that the deployment measures itself. Instances boot with ima_policy=tcb and the Policy Manager appraises what executes against an allowlist its learners built from this exact filesystem. An upgraded package produces a new digest for a path the allowlist pins, enforcement.l3 reports it, and your KMS grant is withdrawn on a machine that is behaving exactly as its vendor intended. The allowlist cannot be made to tolerate it, because refusing unknown executables is what the layer is for. A kernel upgrade additionally moves what the next reboot measures.

So patching is a rebuild, not an update. To take a security fix:

  1. Rebuild the image with infrastructure/packer/polaris-cvm-aws.pkr.hcl, which installs current packages at bake time.
  2. Point ami_id at the new AMI and deploy. Both instances are replaced.
  3. Let the learners converge again before promoting any layer back to enforce. The allowlist describes the old filesystem and has to be rebuilt for the new one.

This is the same rule the rest of the stack already follows. Every version the bake installs is pinned, down to Docker and containerd, and on-premises a firmware fix is a new pinned artifact rather than an in-place update.

What this costs you, stated plainly. Security updates do not arrive on their own. You own the decision of when to rebuild, and a deployment left alone for months is running the packages its image was built with. On a confidential VM whose whole value is that its contents are known and measured, an unreviewed change arriving on someone else's schedule is the worse of the two options, but it is a trade and not a free win. If you need a patching cadence, drive it from your own rebuild schedule.

Teardown​

destroy removes the instances, the VPC resources and the Secrets Manager secrets. Your KMS key stays, because you own it and your data is encrypted under it.

It also retires this deployment's KMS grants, and it has to do that before Terraform runs. Nothing in Polaris retires a grant on shutdown: the Policy Manager retires one on a failing verdict, on expiry and at its own startup, and that is all. Once the instances are gone the grantee role and the retiring principal are gone with them, so no component can ever clear the grant and only a key admin can. Since the key is meant to be reused across deployments, those would accumulate on it, and KMS caps a key at 50,000 grants.

Grants are matched by this deployment's CVM role rather than by the polaris- name prefix, so a key shared with other deployments keeps theirs.

If that step cannot run, because the state no longer has the outputs it needs, the teardown continues and prints the commands to finish by hand. Stopping a teardown on a cleanup step would leave instances running and billing, which is worse than a grant left behind:

aws kms list-grants --key-id <key arn> \
--query "Grants[?starts_with(Name,'polaris-')].[GrantId,GranteePrincipal,Name]" --output text
aws kms revoke-grant --key-id <key arn> --grant-id <id>

Notes​

  • Instance-metadata credentials are firewalled off from the workload container by design, so a compromised workload cannot borrow the instance's IAM identity to reach KMS behind Polaris's back. That is the same hop limit that stops the proxy reading the identity document itself.
  • The report interval on AWS is 30 seconds, against a 120-second dead man's window.
  • Every deploy currently replaces both instances, because the image tag it pins is timestamped (F0X-535).