Skip to content

Commit f413dc5

Browse files
fix(microvm): tighten runner role permissions
1 parent dd59be7 commit f413dc5

6 files changed

Lines changed: 18 additions & 39 deletions

File tree

lambdas/libs/compute-providers/aws/microvm/README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ The MicroVM image `/run` hook receives this `runHookPayload`:
1313
}
1414
```
1515

16-
Lambda adds `microvmId` beside that payload. The image must poll the SecureString parameter at `<runnerConfigSsmPath>/<microvmId>`, start the GitHub runner with its encoded JIT configuration, delete the parameter after reading it, and terminate the MicroVM after the job completes.
16+
Lambda adds `microvmId` beside that payload. The image must poll the SecureString parameter at `<runnerConfigSsmPath>/<microvmId>`, start the GitHub runner with its encoded JIT configuration, delete the parameter after reading it, and exit its lifecycle entrypoint after the job completes. Trusted control-plane cleanup and the fixed lifetime remain the MicroVM termination backstops.
1717

1818
Runner ownership and lifecycle state are stored separately as non-secret `String`
1919
parameters under `<MICROVM_METADATA_SSM_PATH>/<microvmId>`. The immutable base
@@ -52,12 +52,13 @@ resource-level permissions, enforce the connector boundary with the explicit
5252
dynamic-label allowlist described below.
5353

5454
All MicroVMs using one execution role and JIT prefix share a trust boundary.
55-
Grant that role only `ssm:GetParameter` and `ssm:DeleteParameter` on the JIT
56-
prefix; do not grant parameter-listing APIs or access to the metadata prefix.
57-
The `MicrovmId` tag on each JIT parameter supports operations but is not a
58-
documented binding to the calling MicroVM's session identity. Only allow trusted
59-
images and workloads within a shared role, or isolate trust domains with
60-
separate roles, prefixes, and provider deployments.
55+
Grant that role only `ssm:GetParameter`, `ssm:ListTagsForResource`, and
56+
`ssm:DeleteParameter` on the JIT prefix; do not grant parameter-listing APIs or
57+
access to the metadata prefix. The `MicrovmId` tag on each JIT parameter
58+
supports operations but is not a documented binding to the calling MicroVM's
59+
session identity. Only allow trusted images and workloads within a shared role,
60+
or isolate trust domains with separate roles, prefixes, and provider
61+
deployments.
6162

6263
## Dynamic labels
6364

modules/compute-providers/aws/microvm/README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Select it with the `compute_provider.aws.microvm` leaf. The Terraform dispatch k
66

77
MicroVM runners use the provider's fixed 28,800-second (8-hour) lifetime; this is not a Terraform input.
88

9-
The resolved provider-neutral `runner.iam.role` is passed to Lambda as the MicroVM execution role. The provider creates `/github-self-hosted-runners/<prefix>/microvm` with the common observability lifecycle and derives a control-plane-only metadata prefix at `<ssm.paths.root>/<ssm.paths.config>/microvm-metadata`. Scale-up, scale-down, and pool use that non-secret prefix for MicroVM ownership and lifecycle state; the runner role retains access only to its one-time JIT path plus runtime logging and self-termination. When the runner role is supplied externally, its Lambda trust, JIT parameter access, stream-write, and self-termination permissions remain caller-owned.
9+
The resolved provider-neutral `runner.iam.role` is passed to Lambda as the MicroVM execution role. The provider creates `/github-self-hosted-runners/<prefix>/microvm` with the common observability lifecycle and derives a control-plane-only metadata prefix at `<ssm.paths.root>/<ssm.paths.config>/microvm-metadata`. Scale-up, scale-down, and pool use that non-secret prefix for MicroVM ownership and lifecycle state; the runner role retains value, tag-read, and deletion access only on the lane-scoped one-time JIT path, plus runtime logging. When the runner role is supplied externally, its Lambda trust, JIT parameter access, and stream-write permissions remain caller-owned.
1010

1111
<!-- BEGIN_TF_DOCS -->
1212
## Requirements
@@ -37,7 +37,6 @@ No modules.
3737
| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |
3838
| [aws_iam_policy_document.runner_runtime_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
3939
| [aws_iam_policy_document.runner_ssm_jit](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
40-
| [aws_iam_policy_document.runner_terminate_self](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
4140
| [aws_iam_policy_document.scale_down](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
4241
| [aws_iam_policy_document.scale_up](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
4342

@@ -51,7 +50,7 @@ No modules.
5150
| <a name="input_github"></a> [github](#input\_github) | GitHub Enterprise Server settings available to compute-provider bootstrap data.<br/><br/>- `enterprise_server.url`: Optional GitHub Enterprise Server base URL. Null selects GitHub.com.<br/>- `enterprise_server.ssl_verify`: Enables TLS certificate verification for GitHub Enterprise Server. | <pre>object({<br/> enterprise_server = optional(object({<br/> url = optional(string, null)<br/> ssl_verify = optional(bool, true)<br/> }), {})<br/> })</pre> | `{}` | no |
5251
| <a name="input_observability"></a> [observability](#input\_observability) | Provider-neutral observability settings applied to the provider-managed MicroVM runtime log group.<br/><br/>- `logs.retention_in_days`: CloudWatch Logs retention period.<br/>- `logs.kms_key_id`: Optional KMS key ID or ARN used to encrypt the log group.<br/>- `logs.class`: CloudWatch log-group class.<br/>- `logs.tags`: Tags merged after module-level tags on the log group. | <pre>object({<br/> logs = optional(object({<br/> retention_in_days = optional(number, 180)<br/> kms_key_id = optional(string, null)<br/> class = optional(string, "STANDARD")<br/> tags = optional(map(string), {})<br/> }), {})<br/> })</pre> | `{}` | no |
5352
| <a name="input_prefix"></a> [prefix](#input\_prefix) | Prefix used to identify resources created for the runner configuration. | `string` | `"github-actions"` | no |
54-
| <a name="input_runner"></a> [runner](#input\_runner) | Resolved runner settings consumed by the Lambda MicroVM compute provider.<br/><br/>- `os`: Runner operating system. Lambda MicroVM requires `linux`.<br/>- `architecture`: Runner distribution architecture. Lambda MicroVM requires `arm64`.<br/>- `name_prefix`: Prefix added to registered runner names.<br/>- `run_as_root`: Runs the runner service as root.<br/>- `run_as`: Operating-system user used when `run_as_root` is false.<br/>- `hooks.job_started`: Script installed as the runner job-started hook.<br/>- `hooks.job_completed`: Script installed as the runner job-completed hook.<br/>- `iam.role.arn`: Resolved runner-role ARN used as the MicroVM execution role and referenced by provider policies.<br/>- `iam.role.name`: Resolved runner-role name used by provider resources.<br/>- `iam.role.managed`: Whether runner-config manages the resolved runner role. Callers own an external role and must grant it `ssm:GetParameter` and `ssm:DeleteParameter` on the lane token path, `logs:CreateLogStream` and `logs:PutLogEvents` on the provider-managed runtime log group, and `lambda:TerminateMicrovm` on the approved image ARN or image allowlist for self-termination.<br/>- `iam.managed_policy_arns`: Common managed-policy ARNs returned with the provider-specific runner policies for attachment by runner-config.<br/>- `iam.path`: IAM path available to provider-managed IAM resources. Null derives the path from `prefix`. | <pre>object({<br/> os = optional(string, "linux")<br/> architecture = optional(string, "arm64")<br/> name_prefix = optional(string, "")<br/> run_as_root = optional(bool, false)<br/> run_as = optional(string, "ec2-user")<br/> hooks = optional(object({<br/> job_started = optional(string, "")<br/> job_completed = optional(string, "")<br/> }), {})<br/> iam = object({<br/> role = object({<br/> arn = string<br/> name = string<br/> managed = optional(bool, true)<br/> })<br/> managed_policy_arns = optional(map(string), {})<br/> path = optional(string, null)<br/> })<br/> })</pre> | n/a | yes |
53+
| <a name="input_runner"></a> [runner](#input\_runner) | Resolved runner settings consumed by the Lambda MicroVM compute provider.<br/><br/>- `os`: Runner operating system. Lambda MicroVM requires `linux`.<br/>- `architecture`: Runner distribution architecture. Lambda MicroVM requires `arm64`.<br/>- `name_prefix`: Prefix added to registered runner names.<br/>- `run_as_root`: Runs the runner service as root.<br/>- `run_as`: Operating-system user used when `run_as_root` is false.<br/>- `hooks.job_started`: Script installed as the runner job-started hook.<br/>- `hooks.job_completed`: Script installed as the runner job-completed hook.<br/>- `iam.role.arn`: Resolved runner-role ARN used as the MicroVM execution role and referenced by provider policies.<br/>- `iam.role.name`: Resolved runner-role name used by provider resources.<br/>- `iam.role.managed`: Whether runner-config manages the resolved runner role. Callers own an external role and must grant it `ssm:GetParameter`, `ssm:ListTagsForResource`, and `ssm:DeleteParameter` on the lane token path plus `logs:CreateLogStream` and `logs:PutLogEvents` on the provider-managed runtime log group.<br/>- `iam.managed_policy_arns`: Common managed-policy ARNs returned with the provider-specific runner policies for attachment by runner-config.<br/>- `iam.path`: IAM path available to provider-managed IAM resources. Null derives the path from `prefix`. | <pre>object({<br/> os = optional(string, "linux")<br/> architecture = optional(string, "arm64")<br/> name_prefix = optional(string, "")<br/> run_as_root = optional(bool, false)<br/> run_as = optional(string, "ec2-user")<br/> hooks = optional(object({<br/> job_started = optional(string, "")<br/> job_completed = optional(string, "")<br/> }), {})<br/> iam = object({<br/> role = object({<br/> arn = string<br/> name = string<br/> managed = optional(bool, true)<br/> })<br/> managed_policy_arns = optional(map(string), {})<br/> path = optional(string, null)<br/> })<br/> })</pre> | n/a | yes |
5554
| <a name="input_ssm"></a> [ssm](#input\_ssm) | Parameter Store paths and tag scopes available to compute-provider bootstrap resources.<br/><br/>- `paths.root`: Root Parameter Store path for the runner configuration.<br/>- `paths.tokens`: Path segment used for registration tokens and just-in-time configuration.<br/>- `paths.config`: Path segment used for persistent runner and provider configuration. MicroVM control-plane metadata is stored under its `microvm-metadata` child prefix.<br/>- `tags`: Shared SSM tags that override module-level `tags`.<br/>- `parameters.tags`: Parameter-specific tags that override module-level and shared SSM tags. | <pre>object({<br/> paths = object({<br/> root = string<br/> tokens = string<br/> config = string<br/> })<br/> tags = optional(map(string), {})<br/> parameters = optional(object({<br/> tags = optional(map(string), {})<br/> }), {})<br/> })</pre> | n/a | yes |
5655
| <a name="input_tags"></a> [tags](#input\_tags) | Base tags available to taggable compute-provider resources. Provider-specific tags override this map within their documented scopes. | `map(string)` | `{}` | no |
5756

modules/compute-providers/aws/microvm/runner-policies.tf

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ locals {
55
runner_token_path_arn = "${local.ssm_parameter_arn_prefix}${var.ssm.paths.root}/${var.ssm.paths.tokens}/*"
66

77
runner_inline_policies = {
8-
terminate_self = {
9-
name = "runner-microvm-terminate-self"
10-
policy_json = data.aws_iam_policy_document.runner_terminate_self.json
11-
}
128
ssm_jit = {
139
name = "runner-microvm-ssm-jit"
1410
policy_json = data.aws_iam_policy_document.runner_ssm_jit.json
@@ -20,21 +16,13 @@ locals {
2016
}
2117
}
2218

23-
data "aws_iam_policy_document" "runner_terminate_self" {
24-
statement {
25-
sid = "SelfTerminate"
26-
effect = "Allow"
27-
actions = ["lambda:TerminateMicrovm"]
28-
resources = local.microvm_image_resource_arns
29-
}
30-
}
31-
3219
data "aws_iam_policy_document" "runner_ssm_jit" {
3320
statement {
3421
effect = "Allow"
3522
actions = [
3623
"ssm:DeleteParameter",
3724
"ssm:GetParameter",
25+
"ssm:ListTagsForResource",
3826
]
3927
resources = [local.runner_token_path_arn]
4028
}

modules/compute-providers/aws/microvm/tests/provider.tftest.hcl

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,7 @@ run "exposes_microvm_control_plane_contract" {
181181
assert {
182182
condition = (
183183
toset(keys(output.provider.policies)) == toset(["runner", "scale_up", "scale_down", "pool"])
184-
&& toset(keys(output.provider.policies.runner.inline_policies)) == toset(["runtime_logs", "ssm_jit", "terminate_self"])
185-
&& output.provider.policies.runner.inline_policies.terminate_self.name == "runner-microvm-terminate-self"
184+
&& toset(keys(output.provider.policies.runner.inline_policies)) == toset(["runtime_logs", "ssm_jit"])
186185
&& output.provider.policies.runner.inline_policies.ssm_jit.name == "runner-microvm-ssm-jit"
187186
&& output.provider.policies.runner.inline_policies.runtime_logs.name == "runner-microvm-runtime-logs"
188187
&& output.provider.policies.runner.managed_policy_arns["readonly"] == "arn:aws:iam::aws:policy/ReadOnlyAccess"
@@ -194,17 +193,13 @@ run "exposes_microvm_control_plane_contract" {
194193

195194
assert {
196195
condition = (
197-
data.aws_iam_policy_document.runner_terminate_self.statement[0].sid == "SelfTerminate"
198-
&& data.aws_iam_policy_document.runner_terminate_self.statement[0].effect == "Allow"
199-
&& data.aws_iam_policy_document.runner_terminate_self.statement[0].actions == toset(["lambda:TerminateMicrovm"])
200-
&& data.aws_iam_policy_document.runner_terminate_self.statement[0].resources == toset(["arn:aws:lambda:eu-west-1:123456789012:microvm-image:runner"])
201-
&& data.aws_iam_policy_document.runner_ssm_jit.statement[0].actions == toset(["ssm:DeleteParameter", "ssm:GetParameter"])
196+
data.aws_iam_policy_document.runner_ssm_jit.statement[0].actions == toset(["ssm:DeleteParameter", "ssm:GetParameter", "ssm:ListTagsForResource"])
202197
&& data.aws_iam_policy_document.runner_ssm_jit.statement[0].resources == toset(["arn:aws:ssm:eu-west-1:123456789012:parameter/github-action-runners/tokens/*"])
203198
&& length(data.aws_iam_policy_document.runner_runtime_logs.statement) == 1
204199
&& data.aws_iam_policy_document.runner_runtime_logs.statement[0].actions == toset(["logs:CreateLogStream", "logs:PutLogEvents"])
205200
&& data.aws_iam_policy_document.runner_runtime_logs.statement[0].resources == toset(["arn:aws:logs:eu-west-1:123456789012:log-group:/github-self-hosted-runners/microvm-test/microvm:*"])
206201
)
207-
error_message = "Managed MicroVM runners must receive self-termination, lane-token JIT access, and stream-write permissions on the provider-managed runtime log group."
202+
error_message = "Managed MicroVM runners must receive lane-token value, tag, and deletion access plus stream-write permissions on the provider-managed runtime log group."
208203
}
209204

210205
assert {
@@ -308,10 +303,7 @@ run "accepts_external_runner_role_and_policy_overrides" {
308303

309304
assert {
310305
condition = (
311-
toset(keys(output.provider.policies.runner.inline_policies)) == toset(["runtime_logs", "ssm_jit", "terminate_self"])
312-
&& data.aws_iam_policy_document.runner_terminate_self.statement[0].resources == toset([
313-
"arn:aws:lambda:eu-west-1:123456789012:microvm-image:runner-*",
314-
])
306+
toset(keys(output.provider.policies.runner.inline_policies)) == toset(["runtime_logs", "ssm_jit"])
315307
&& length(data.aws_iam_policy_document.runner_runtime_logs.statement) == 1
316308
&& data.aws_iam_policy_document.runner_runtime_logs.statement[0].resources == toset(["arn:aws:logs:eu-west-1:123456789012:log-group:/github-self-hosted-runners/microvm-test/microvm:*"])
317309
)

modules/compute-providers/aws/microvm/variables.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ variable "runner" {
8282
- `hooks.job_completed`: Script installed as the runner job-completed hook.
8383
- `iam.role.arn`: Resolved runner-role ARN used as the MicroVM execution role and referenced by provider policies.
8484
- `iam.role.name`: Resolved runner-role name used by provider resources.
85-
- `iam.role.managed`: Whether runner-config manages the resolved runner role. Callers own an external role and must grant it `ssm:GetParameter` and `ssm:DeleteParameter` on the lane token path, `logs:CreateLogStream` and `logs:PutLogEvents` on the provider-managed runtime log group, and `lambda:TerminateMicrovm` on the approved image ARN or image allowlist for self-termination.
85+
- `iam.role.managed`: Whether runner-config manages the resolved runner role. Callers own an external role and must grant it `ssm:GetParameter`, `ssm:ListTagsForResource`, and `ssm:DeleteParameter` on the lane token path plus `logs:CreateLogStream` and `logs:PutLogEvents` on the provider-managed runtime log group.
8686
- `iam.managed_policy_arns`: Common managed-policy ARNs returned with the provider-specific runner policies for attachment by runner-config.
8787
- `iam.path`: IAM path available to provider-managed IAM resources. Null derives the path from `prefix`.
8888
EOT

modules/runner-config/tests/pool.tftest.hcl

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -711,12 +711,11 @@ run "routes_lambda_microvm_provider" {
711711
&& length(module.compute_aws_microvm) == 1
712712
&& length(module.compute_aws_microvm_trust_policy) == 1
713713
&& aws_iam_role.runner[0].assume_role_policy == module.compute_aws_microvm_trust_policy[0].assume_role_policy
714-
&& toset(keys(aws_iam_role_policy.runner_provider)) == toset(["runtime_logs", "ssm_jit", "terminate_self"])
714+
&& toset(keys(aws_iam_role_policy.runner_provider)) == toset(["runtime_logs", "ssm_jit"])
715715
&& aws_iam_role_policy.runner_provider["runtime_logs"].name == "runner-microvm-runtime-logs"
716716
&& aws_iam_role_policy.runner_provider["ssm_jit"].name == "runner-microvm-ssm-jit"
717-
&& aws_iam_role_policy.runner_provider["terminate_self"].name == "runner-microvm-terminate-self"
718717
)
719-
error_message = "The aws.microvm leaf must dispatch only to the namespaced provider modules and attach all three required policies to its managed runner role."
718+
error_message = "The aws.microvm leaf must dispatch only to the namespaced provider modules and attach both required policies to its managed runner role."
720719
}
721720

722721
assert {

0 commit comments

Comments
 (0)