Repository navigation
[PRODENG-3929] Scope example ingress, split node roles, stop inlining creds - #689
Conversation
… creds Terraform half of PRODENG-3929. The test-side companion that supplies the new required variable is held until PR #686 merges, because it edits the same four files. aws-simple/launchpad.tf accepted every protocol and port from 0.0.0.0/0 on every manager, worker and MSR node, in a rule the file labelled [BAD RULE], and that module is what every CI smoke run provisions. External ingress is now limited to 22 and 5986 from var.admin_cidrs, and 443 and 6443 from var.admin_cidrs plus the VPC CIDR. The VPC CIDR is required on the two load-balanced ports because the MKE and MSR ingresses reach the nodes from inside the VPC rather than from an operator address, and their health checks would fail without it. Node-to-node traffic within the security group is unchanged. aws-complete/sg.tf opened 22 and 5985-5986 from anywhere; both are now sourced from var.admin_cidrs, and 5985 is removed rather than narrowed, because the Windows user data disables the HTTP listener and deletes its firewall rules, so the cleartext port is already dead on the instance. aws-complete's manager group served 443 and 6443 from anywhere; those follow the same admin_cidrs plus VPC CIDR rule. var.admin_cidrs is declared in both examples with no default and a non-empty validation, so a caller must state who may reach the cluster. A default would reintroduce the open rule silently. var.windows_password is declared sensitive in both examples. That masks terraform's own output only: the value still reaches the instance through user data, which IMDS serves unauthenticated, and the provider does not mark aws_launch_template.user_data sensitive. It does not satisfy the ticket's AC-2 and the PR says so. aws-complete used one IAM role for every node, so workers held the cloud-controller-manager permissions they never use. Managers and workers now carry separate roles and instance profiles, assigned by nodegroup role, which the provisioning module supports because instance_profile_name is set per nodegroup. The CCM control-plane policy attaches to the manager role only; the worker policy, which is ECR pull access, attaches to both because every node pulls images, as does the EBS CSI policy that was previously on the single role. The CCM policy is split into discovery actions, which AWS supports no resource-level permissions for, creation actions, which cannot be constrained by the tags of a resource that does not exist yet, and mutation of existing resources, which is constrained to resources carrying this cluster's kubernetes.io/cluster tag. The shape follows the policy published by the upstream cloud-provider-aws project; no ARNs are invented. Both examples rendered the MKE admin password and the WinRM password inline into the generated launchpad.yaml, contradicting the guidance added in PRODENG-3935 that credentials come from the environment. They now emit LAUNCHPAD_MKE_ADMIN_PASSWORD and LAUNCHPAD_WINRM_PASSWORD references, which launchpad expands through envsubst at config load; an unset variable fails loudly rather than installing with an empty password. Refs: PRODENG-3929 Written by AI: claude-opus-5 Signed-off-by: James Nesbitt <jnesbitt@mirantis.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unrestricted tagging allows ownership-check bypasses that expose unrelated AWS resources to modification or deletion, while an incorrect Classic ELB tag condition breaks CCM reconciliation.
2 open issues
What changed in this PR
This PR hardens Launchpad’s AWS Terraform examples by narrowing administrative ingress, separating node permissions, and replacing inline credentials in generated YAML. It remains a draft pending companion test changes after #686; Windows password removal from user data is deferred.
Changes:
- Require explicit
admin_cidrs, restrict external ingress, and remove cleartext WinRM ingress. - Separate manager and worker IAM roles in
aws-completeand introduce tag-based CCM mutation restrictions. - Mark Windows passwords sensitive and reference environment variables in generated YAML.
| File | Description |
|---|---|
examples/terraform/aws-simple/variables.tf |
Require administrative CIDRs and mark Windows passwords sensitive. |
examples/terraform/aws-simple/terraform.tfvars.template |
Add an administrative CIDR example. |
examples/terraform/aws-simple/launchpad.tf |
Restrict ingress and emit credential environment references. |
examples/terraform/aws-complete/variables.tf |
Require administrative CIDRs and mark Windows passwords sensitive. |
examples/terraform/aws-complete/terraform.tfvars.template |
Add an administrative CIDR example. |
examples/terraform/aws-complete/sg.tf |
Restrict SSH and HTTPS WinRM; remove port 5985. |
examples/terraform/aws-complete/provision.tf |
Select instance profiles by node role. |
examples/terraform/aws-complete/modules/ebs-csi-driver/variables.tf |
Accept multiple IAM role names. |
examples/terraform/aws-complete/modules/ebs-csi-driver/iam.tf |
Attach the CSI policy to each supplied role. |
examples/terraform/aws-complete/modules/aws-ccm/variables.tf |
Separate control-plane and workload role inputs. |
examples/terraform/aws-complete/modules/aws-ccm/iam.tf |
Split permissions and add ownership-tag conditions. |
examples/terraform/aws-complete/main.tf |
Wire manager and worker roles into policy modules. |
examples/terraform/aws-complete/launchpad.tf |
Restrict manager API ingress and emit credential environment references. |
examples/terraform/aws-complete/iam.tf |
Create separate manager and worker roles and profiles. |
Review effort: Balanced
Review finding on PR #689. ec2:CreateTags and elasticloadbalancing:AddTags were granted without a condition, which defeated the tag condition on the mutation statement: a holder of the manager role could tag an unrelated security group, volume or load balancer with this cluster's kubernetes.io/cluster tag and then mutate or delete it under a statement that reads as constrained. Both actions are now conditioned on their service's CreateAction key, so tags can only be applied as part of the resource-creating calls this policy already grants. ec2:CreateAction is limited to CreateSecurityGroup and CreateVolume; elasticloadbalancing:CreateAction to CreateListener, CreateLoadBalancer and CreateTargetGroup. Both condition keys are listed for those actions in the AWS service reference, and the construction matches the AWS-managed AmazonEBSCSIDriverPolicy. Not verified without a live cluster: whether the cloud controller manager only ever tags during creation. If it tags an existing resource in a separate call, that call is now denied. The PR records this. Refs: PRODENG-3929 Written by AI: claude-opus-5 Signed-off-by: James Nesbitt <jnesbitt@mirantis.com>
Finding A from the post-merge review of #680, authorised into this ticket. aws-complete/provision.tf merged the platform data over the nodegroup definition, so the platform's user_data always won and a caller-supplied per-nodegroup user_data was discarded without warning. The composition and its rationale are copied verbatim from aws-simple/provision.tf rather than reimplemented: a caller's user_data replaces the platform default on ssh platforms, and is appended after it on winrm platforms, because there the platform default resets the local Administrator password and stands up the WinRM HTTPS listener launchpad needs in order to connect at all. Verified with terraform validate against the aws-complete example, with the helm provider pinned to ~> 2.17 in the scratch copy to work around the pre-existing provider-v3 failure on main. No apply. Refs: PRODENG-3929 Written by AI: claude-opus-5 Signed-off-by: James Nesbitt <jnesbitt@mirantis.com>
Authorised by user ruling off w2's measurement on the empty-password fold, not from PRODENG-3929's text. Both examples defaulted windows_password to "" and both generators emit a password line unconditionally, so a stack with Windows nodegroups and no windows_password produced a launchpad.yaml instructing launchpad to authenticate as Administrator with an empty password. The ruling was to remove the default. Measured, that breaks stacks with no Windows node at all: test/smoke/smoke_test.go passes the variable only when a nodegroup platform starts with "windows_", so the three Linux-only smoke tests pass nothing; swarm_addr_pool_test.go, upgrade_test.go, integration_test.go and test_manager.go reference it zero times; and both terraform.tfvars.template files ship it commented out. Five call paths and both documented starting points would fail at plan on Linux-only stacks. The default is therefore null with a validation that fires only when a nodegroup uses a windows_ platform, which is the detection smoke_test.go:152 already uses. It fails at plan naming the variable and explaining what it is for, and it rejects an explicitly-passed empty string, which is the case indistinguishable from the default in the generated YAML. Linux-only stacks continue to plan. Consumers take a string, so each example derives local.windows_password, which substitutes "" for null; the validation guarantees that only happens when no Windows node is being provisioned. Cross-variable references in validation require Terraform 1.9 or later, stated in the variable description. Verified by plan against the aws-simple example: a Linux-only config raises no variable error; a config whose nodegroup platform is windows_2022 and which passes no password fails with "Invalid value for variable ... windows_password must be set to a non-empty value when any nodegroup uses a windows_ platform"; the same config passing windows_password = "" fails identically; a Linux-only config that does pass a password raises no error. aws-complete validates. No apply. Refs: PRODENG-3929 Written by AI: claude-opus-5 Signed-off-by: James Nesbitt <jnesbitt@mirantis.com>
Signed-off-by: James Nesbitt <jnesbitt@mirantis.com> # Conflicts: # examples/terraform/aws-simple/platform.tf
…ites AC-4: the suites that provision these examples must keep working with the narrowed rules. This is the companion to the terraform changes on this branch, held until #686 merged because it edits the same files. The examples' admin_cidrs variable is required with no default, so every caller must state who may reach the cluster. test.AdminCIDRs reads SMOKE_ADMIN_CIDRS as a comma-separated list and falls back to 0.0.0.0/0 when it is unset, logging that it did. The fallback is the exposure the examples had before, but stated at the caller where it can be found and changed, rather than implied by a module default. Runners have no fixed egress address, and discovering one at run time would add an unauthenticated network call to every smoke run. The examples now emit LAUNCHPAD_MKE_ADMIN_PASSWORD and LAUNCHPAD_WINRM_PASSWORD references instead of inlining credentials, so the suites export them with test.ExportGeneratedCredentials before the generated YAML is parsed; launchpad resolves them through envsubst at load, and an unset reference fails loudly. TestGeneratedCredentialReferencesMatchExports pins the contract across the two languages: it extracts every $${NAME} reference from both examples' launchpad.tf and asserts each is a variable the suites export. Renaming either side would otherwise produce a config whose credentials expand to nothing, which only a live cluster would reveal. Verified by mutation: renaming the reference in aws-simple/launchpad.tf fails the test with "refers to LAUNCHPAD_MKE_ADMINPASSWORD, which no suite exports". TestAdminCIDRs covers the fallback and a configured list. Not demonstrated: that a smoke run passes end to end. Smoke needs live AWS and is gated, so what is shown is that the suites supply the variable and export the references, not that a provisioned cluster comes up. Refs: PRODENG-3929 Written by AI: claude-opus-5 Signed-off-by: James Nesbitt <jnesbitt@mirantis.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unscoped ELB listener and policy permissions bypass cluster ownership boundaries, while reapplying existing stacks can remove running nodes’ AWS permissions without migrating their instance profiles.
4 open issues
2 resolved since last review
Review effort: Balanced
…agging Review findings on PR #689. CreateLoadBalancerListeners and CreateLoadBalancerPolicy operate on an existing Classic load balancer despite their names, so granting them unconditionally let the manager role add listeners and policies to load balancers this cluster does not own. Both move to the tag-constrained mutation statement. The cloud controller manager tags an existing load balancer during reconciliation when a Service carries additional resource tags (addLoadBalancerTags), and that request carries no CreateAction, so the create-only grant would have failed reconciliation. A second statement allows elasticloadbalancing:AddTags on load balancers already carrying this cluster's tag. The escalation stays closed: a resource that is not already this cluster's still cannot be tagged into it. windows_password's validation rejected an explicitly-passed empty string on Linux-only stacks, which have no host that would authenticate with it. The whole requirement is now gated on a Windows nodegroup being present, in both examples. The IAM role split replaces an existing stack's IAM resources, and provision-aws does not refresh running instances on a launch-template change, so nodes provisioned before this change keep an instance profile whose role has been destroyed and lose their permissions until replaced. That is recorded at the top of aws-complete/iam.tf. Enabling instance_refresh is deliberately not done here: it is a separate behavioural decision tracked under PRODENG-4062. Verified by plan against aws-simple, counting "Invalid value for variable": Linux-only 0, Linux-only with windows_password = "" 0 (previously 1), Windows nodegroup without a password 1, Windows with windows_password = "" 1, Windows with a password 0. aws-complete validates. No apply. Refs: PRODENG-3929 Written by AI: claude-opus-5 Signed-off-by: James Nesbitt <jnesbitt@mirantis.com>
james-nesbitt
left a comment
There was a problem hiding this comment.
most of this PR is good , but you have changed the examples input significantly in ways that will break existing external usecases and make it harder to run.
Things that are good:
- firewall tuning
- better ingress control (CIDR tuning)
- better passowrd generation and handoff
But in your attempt to push for top level security the following are unacceptable:
- defaulting to an unknown IP for admin access (is that IP a leak)? - yes defaulting to 0.0.0.0/0 produces a risk, but the risk is documented and easy to handle.
- using only ENV variables hides an important input - ENV variables should augment to allow security improvements, but not make it harder to use. Doesn't Terraform already have a pattern for injecting inputs as ENV variables?
…rs, variable credentials The security work on this branch changed the examples' input surface in two ways that break an existing external tfvars. This returns both to a shape that keeps working, without giving up the firewall scoping, the CIDR ingress control or the password handling the change was for. admin_cidrs now defaults to ["0.0.0.0/0"] in aws-simple and aws-complete instead of being required. A tfvars written against main previously failed at variable resolution with "No value for required variable"; it now plans. The default is open, which is a real risk, so it is documented as such at the declaration, in the tfvars template and in each example's README, together with the one-line change that narrows it. The 203.0.113.4/32 placeholder is gone: it was RFC 5737 documentation space that looked like a real address and answered no question the reader had. Credentials go back to being written from the terraform variables that carry them, so the generated launchpad.yaml is usable with nothing exported into the environment. Supplying them from the environment is still available and is now documented where a user will look, through terraform's own mechanism rather than a launchpad-specific one: TF_VAR_launchpad carries the object as JSON because the MKE password is nested inside it, and TF_VAR_windows_password carries the Windows credential. The environment augments the variables; it is no longer the only route. The harness helper that exported LAUNCHPAD_MKE_ADMIN_PASSWORD and LAUNCHPAD_WINRM_PASSWORD is removed along with its four call sites, and the test that pinned those references is removed rather than re-pinned: the generated document no longer refers to the environment, so the contract it asserted no longer exists. TestAdminCIDRs and the SMOKE_ADMIN_CIDRS override stay. windows_password keeps its sensitive marking and its Windows-gated validation. var.launchpad is left exactly as main has it. Written by AI: claude-opus-5 Signed-off-by: James Nesbitt <jnesbitt@mirantis.com>
…ls (#703) * [PRODENG-3929] Admit the NodePort range the aws-simple example installs Second PR on PRODENG-3929. #689 replaced aws-simple's blanket `protocol = "-1"` ingress rule with four scoped ones -- 22 and 5986 from var.admin_cidrs, 443 and 6443 from admin_cidrs plus the VPC CIDR. The scoping is right and stays. What went with the blanket rule, and was not replaced, is the Kubernetes NodePort range: launchpad.tf installs MKE with --nodeport-range=32768-35535 and no rule admitted it at any CIDR, so a re-apply produced a cluster declaring a NodePort range nothing could reach, with no tfvars value that restored it. Measured rather than assumed. The pre-#689 external rule was from_port 0 / to_port 0 / protocol "-1" from 0.0.0.0/0, which in AWS means every port of every protocol, so the literal lost set is everything outside the four new rules. Of the ports this repo actually names, only the NodePort range needs an external rule: - 2377/tcp is node-to-node; SwarmAddress is built from the host's InternalAddress (pkg/product/mke/config/host.go:388) - 7946 tcp+udp, 4789/udp and 10250/tcp appear only in the host-firewall user_data, all node-to-node - 2376/tcp is referenced nowhere in the repo at all All of those are already carried by the self=true rule, which #689 left in place at launchpad.tf:83-90 and which covers every nodegroup, so re-opening them externally would be a widening with no consumer. The range is now stated once, as local.nodeport_range, and feeds both the security group rule and the --nodeport-range install flag. The two previously could not disagree only because one of them did not exist; sharing the local means they still cannot. Source is a new var.nodeport_cidrs rather than admin_cidrs. NodePort services are reached by application clients, who are not the administrators admin_cidrs names: with one list, admitting app clients also hands them SSH and the MKE and Kubernetes APIs. It defaults to null and falls back to admin_cidrs through a local, so an existing tfvars is unchanged and a default run restores exactly the pre-#689 exposure of that range. An empty list is rejected, because a cluster installed with --nodeport-range and no way in is the failure the variable exists to prevent. The rule covers TCP and UDP -- both worked before #689 and a UDP NodePort service is legal -- and takes no VPC CIDR, because nothing in local.launchpad_ingresses load-balances the range, so there is no in-VPC health check to admit. Verified without AWS, with AWS_CONFIG_FILE, AWS_SHARED_CREDENTIALS_FILE and AWS_EC2_METADATA_DISABLED pinned off throughout. terraform init, validate and fmt -check pass on aws-simple and on untouched aws-complete. A plan against a tfvars reconstructed from the template resolves every variable, plans the three non-AWS resources and stops at "No valid credential sources found", which is as far as it can go. terraform console renders the rule list as tcp 22, tcp 5986, tcp 443, tcp 6443, then tcp 32768-35535 and udp 32768-35535; with admin_cidrs=["198.51.100.17/32"] and nodeport_cidrs=["203.0.113.0/24"] the NodePort rules carry 203.0.113.0/24 while the rest carry 198.51.100.17/32; nodeport_cidrs=[] fails at plan with the validation message; and the flag renders "--nodeport-range=32768-35535", byte-identical to the literal it replaces. No apply, no AWS call of any kind. Two findings left out of this change deliberately, both recorded on the PR. The tfvars user_data opens 2377, 7946, 4789 and 10250 but not the NodePort range, so on rhel_9 and sles_15 firewall-cmd still blocks it whatever the security group says -- that predates #689 and is noted in the README rather than fixed here. And aws-complete declares the same --nodeport-range with no rule for it, before and after #689; it lost nothing, so it is not this regression. Refs: PRODENG-3929 Written by AI: claude-opus-5 Signed-off-by: James Nesbitt <jnesbitt@mirantis.com> * PRODENG-3929: note the ubuntu_22.04 host-firewall gap too The NodePort host-firewall caveat singled out rhel_9 and sles_15. Both ubuntu_22.04 user_data entries in terraform.tfvars.template carry the identical omission: `sudo ufw allow 2377,7946,10250/tcp; sudo ufw allow 7946,4789/udp`, with no 32768-35535. Say so, and mark the ufw-active precondition as the inference it is — it cannot be tested without AWS. README only; no terraform changed. Written by AI: claude-opus-5 Signed-off-by: James Nesbitt <jnesbitt@mirantis.com> --------- Signed-off-by: James Nesbitt <jnesbitt@mirantis.com>


Ready for review.
Updated 2026-11-06 after the user's review of this PR (
a8c4860). Two things this PR originally did were rejected as breaking existing external usecases, and have been reverted to a non-breaking shape at his instruction:admin_cidrswas a required variable with no default, and the generatedlaunchpad.yamlreferenced credentials only by environment variable. Both are fixed below, and the claims in this body have been rewritten to match what the branch now does rather than what it did. The firewall tuning, the CIDR ingress scoping and the password generation/handoff he named as good are unchanged.This PR was draft until its test-side companion could land, because the suites that provision these examples are in
test/smoke/*.go,test/integration/andtest/test_manager.go, which PR #686 also edited. #686 merged, the companion landed at259de1d, and the branch was brought up to date withmainby a merge commit (a85b720).What
aws-simple/launchpad.tf: external ingress scoped to 22 and 5986 fromvar.admin_cidrs, and 443/6443 fromvar.admin_cidrsplus the VPC CIDR. Node-to-node traffic inside the group unchanged.aws-complete/sg.tf: 22 and WinRM fromvar.admin_cidrs; 5985 removed, not narrowed.aws-complete/launchpad.tf: manager 443/6443 scoped the same way.var.admin_cidrsadded to both examples, defaulting to["0.0.0.0/0"]and documented as open. See "Whyadmin_cidrsdefaults open" below; this is deliberate and at the user's instruction, not an oversight. The non-empty validation is kept, so an explicit empty list is still rejected. Documented at the declaration, in bothterraform.tfvars.templatefiles and in each example'sREADME.md, with the one-line change that narrows it.var.windows_passworddeclaredsensitive = truein both examples, and its empty default replaced bydefault = nullplus a validation that fires only when a nodegroup uses awindows_platform. Each example deriveslocal.windows_password, substituting""for null, because consumers take a string.aws-complete: separate manager and worker IAM roles and instance profiles, assigned by nodegroup role. CCM control-plane policy attached to managers only; worker (ECR) policy and the EBS CSI policy attached to both.modules/aws-ccm/iam.tf: singleResource: *statement split into discovery, creation, tagging and tag-constrained mutation.ec2:CreateTagsandelasticloadbalancing:AddTagsare conditioned onec2:CreateAction/elasticloadbalancing:CreateAction, so a tag can only be applied as part of a create call this policy grants; an unconditioned tagging grant would let the role tag an unrelated resource as cluster-owned and then mutate it under the condition meant to constrain it.Not verifiable here: whether the cloud controller manager only ever tags during creation. If it tags an already-created resource in a separate call, that call is now denied.
aws-completehas no smoke coverage, so this has no gate behind it.Both examples: the generated
launchpad.yamlis written from the terraform variables that carry the credentials —var.launchpad.mke_connect.passwordandvar.windows_password— so the document is usable with nothing exported into the environment. Supplying them from the environment is still available, through Terraform's own mechanism rather than a launchpad-specific one:TF_VAR_launchpadcarries the object as JSON because the MKE password is nested inside it, andTF_VAR_windows_passwordcarries the Windows credential. Documented in both templates, both READMEs and above each emission block. The environment augments the variables; it is not the only route.aws-complete/provision.tf: a caller-supplied per-nodegroupuser_datais no longer discarded. The platform data was merged over the nodegroup definition, so the platform default always won. The composition and its rationale are copied verbatim fromaws-simple/provision.tf: a caller'suser_datareplaces the platform default on ssh platforms and is appended after it on winrm platforms, where the platform default resets the Administrator password and stands up the WinRM HTTPS listener launchpad needs to connect at all. This is Finding A from the post-merge review of Adopt provision-aws 0.3.0 in the terraform examples #680, found after that PR merged and authorised into this ticket; it is not part of PRODENG-3929's original text.Why
The
aws-simplerule the file itself labelled[BAD RULE]accepted every protocol and port from0.0.0.0/0on every node, and that module is what every CI smoke run provisions.aws-completeopened 22 and 5985-5986 from anywhere, 5985 being cleartext WinRM. One IAM role for all nodes meant workers held cloud-controller-manager permissions — security-group, route, volume and load-balancer mutation atResource: *— that only the control plane uses.Why
admin_cidrsdefaults open. It defaults to["0.0.0.0/0"]on purpose, at the user's instruction. An earlier revision of this PR made it required with no default, on the reasoning that a default would reintroduce the open rule silently; that broke every existing externalterraform.tfvars, which failed at variable resolution before any provider call. The ruling is that defaulting to0.0.0.0/0does produce a real risk, but the risk is documented and easy to handle, whereas a required variable makes the example harder to use and breaks callers. So the exposure is kept as the default and stated plainly wherever a user will look. The VPC CIDR is required on 443/6443 because the MKE and MSR load balancers reach nodes from inside the VPC, and health checks would fail if onlyadmin_cidrswere allowed.The inline-credential change is withdrawn. It came from PRODENG-3935's review rather than this ticket's text: 3935 documented sourcing credentials from the environment while the repo's own examples inlined them. The shape this PR used to close that finding — referencing
LAUNCHPAD_MKE_ADMIN_PASSWORDandLAUNCHPAD_WINRM_PASSWORDfrom the generated YAML, so the environment was the only route — is the shape the user ruled against, because it hides an important input and makes the example harder to run. That finding is therefore open again and is not closed by this PR.TF_VAR_<name>is offered as the documented way to keep a secret off disk, but it is an option, not a requirement, so it does not close a finding about credentials being inlined.Known consequence of the ingress scoping — NodePort and the swarm ports
Stated here because a reader will otherwise reconstruct it wrongly, and because defaulting
admin_cidrsto0.0.0.0/0does not hide it.main'saws-simplerule was protocol-1, ports 0-0, from0.0.0.0/0— every port on every node. Replacing it with four scoped rules changes which ports exist, not only who may reach them. So NodePort32768-35535, swarm2377and docker2376now have no rule at all, at any CIDR. Defaultingadmin_cidrsto0.0.0.0/0restores the breadth of the sources allowed to reach 22, 5986, 443 and 6443; it does not restore those other ports, and notfvarsvalue can.This example declares
--nodeport-range=32768-35535in its MKEinstallFlags(aws-simple/launchpad.tf:234) while no longer admitting traffic to that range. A user whose workloads are reached by NodePort will find them unreachable after re-applying.This is recorded as a known consequence of the scoping, not as a defect being fixed here. Widening the rules back is a decision for the user, and no fifth rule is added in this PR.
What this remediates, and what it does not
Everything here changes what a future
terraform applyproduces. It does not alter infrastructure that already exists:0.0.0.0/0ingress until something re-applies these rules against it.sensitive = truechanges what terraform prints from now on. It cannot retract output already written by past runs, wherever that output was captured, and it does not change a running instance's launch template.terraform validateon a scratch copy is structurally a creation-path proof: it says the configuration is valid to apply, not that any deployed stack has been remediated. Operators of existing clusters need a re-apply, and for the user-data item a replacement of the Windows instances.AC-2 — specified, not implemented, pending a decision
Base note:
mainhas moved to51e98a6("Adopt provision-aws 0.3.0 in the terraform examples", #680) since this branch was cut. That commit deletedexamples/terraform/aws-simple/userdata_windows.tpland moved the Windows user-data rendering into the upstream module. It relocated this exposure rather than removing it: both examples still passvar.windows_passwordtomodules/platform(aws-simple/platform.tf:34,aws-complete/platform.tf:16), and provision-aws v0.3.0 generates the Administrator-password-reset user data itself from that variable — see the comment #680 added ataws-simple/platform.tf:60-66. The password still reaches EC2 user data; only the code that writes it moved out of this repository. This branch does not touch that file, which is why it remainsMERGEABLEacross the deletion.PRODENG-3929's ticket text cites
aws-simple/userdata_windows.tpl:11-13as the rendering site. That citation is stale onmainas of51e98a6. The ticket is not edited here.The deciding fact is coverage asymmetry.
aws-simpleis the only example any smoke suite exercises — everytest/smoke/*.gopoints at it — so implementing the password-data path there puts an unverifiable timing dependency on the single path that can break a smoke run.aws-completehas no smoke coverage at all, its key module is ed25519, which cannot decrypt EC2 password data, and it does not currentlyterraform validate(see Testing).Two ways to close it, and only one is inside this repository's control:
provision-awsmodules/platformwould have to stop deriving user data fromwindows_password, or accept a mode that does not. Not actionable here; it is a different repository.windows_passwordat all and sources the credential from the EC2 password-data API instead. This is the in-repo option.Option 2 is implementable, verified from source rather than assumed:
provision-awsexposesnodegroups[k].nodes[*].instance_id(nodes.tf:60-74).aws_instancecarriespassword_dataand acceptsget_password_data(confirmed fromterraform providers schema -json).aws-simple/key.tfalready generates RSA-4096 whenssh_key_algorithm = "rsa", which the Windows smoke tests set.So the path is: stop passing
windows_passwordintomodule.platform, let EC2Launch keep the random Administrator password it sets, read it withget_password_data,rsadecryptit withtls_private_key.this.private_key_pem, and feed that into the generated YAML. Since51e98a6this is the whole of the change on this side — there is no longer a local template whose password-setting block could simply be deleted.The unresolved risk:
password_datais only available once the Windows agent posts it, typically minutes after boot, and a data source does not retry the way the resource'sget_password_datawait does. Read too early it is empty and WinRM auth fails. In-repo mitigations exist —depends_onagainst the nodegroup, ortime_sleep— but whether the timing holds on a real Windows 2019/2022/2025 boot is only measurable with a live apply, which is gated.sensitive = trueis applied in both examples because AC-2 requires it separately. It does not satisfy AC-2. The value still reachesaws_launch_template.user_data, which the provider schema does not marksensitive. That is a schema and module-source reading, not an observation of any run: it establishes that nothing in that chain suppresses the value, so an apply's output can carry the base64 user data and a reader who has it canbase64 -dit. Whether any specific stored log contains it has not been checked. IMDS separately serves instance user data to processes on the instance.AC-3 — why
aws-simplekeeps a single roleThe role split is applied to
aws-completeonly. Inaws-simple,iam.tfcreatescommon_roleandcommon_profileand no policy anywhere references that role —grep -rn 'common_role\|iam_role_name' examples/terraform/aws-simplereturns onlyiam.tf:1(the role) andiam.tf:22(the profile's reference to it); the only consumer isprovision.tf:46, which assigns the instance profile. AC-3 asks for separate roles so the CCM policy attaches to managers only; with no policy attached there is nothing to misattach, and splitting an unused role would add surface without changing any permission.test/terraform.go,test/smoke/*.go,test/integration/integration_test.go: the suites passadmin_cidrs(test.AdminCIDRs, fromSMOKE_ADMIN_CIDRS, falling back to0.0.0.0/0with a log line). TheExportGeneratedCredentialshelper, the two credential env constants and their four call sites are removed, because the generated YAML no longer refers to the environment.TestGeneratedCredentialReferencesMatchExportsis deleted rather than re-pinned, and coverage was not quietly dropped: it asserted that the.tffiles emit$${NAME}references matching the Go exports, and with the emission reverted that contract no longer exists. What would be left is an assertion that a.tffile does not contain a$${}pattern — source text, guarding a mechanism this commit deletes, and it would misfire the first time anyone uses$${}legitimately.TestAdminCIDRsand theSMOKE_ADMIN_CIDRSoverride stay.Where each change came from
This PR carries three things and only the first is in PRODENG-3929's text.
sensitive = true(part of AC-2). The credential-inlining change carried in from PRODENG-3935's review was part of this PR and has been withdrawn; see "The inline-credential change is withdrawn" above.user_datadiscarded inaws-complete, found in the post-merge review of Adopt provision-aws 0.3.0 in the terraform examples #680 and authorised into this ticket. Landed atc3b75b1.windows_passwordempty default — authorised by user ruling, off a measurement made while validating the empty-password fold in [PRODENG-3935] Warn on insecure WinRM with an inline password #682. Landed ate4df886.On item 3, the literal ruling was remove the default, and this does not do that. Measured, an unconditional requirement fails at plan on stacks with no Windows node:
test/smoke/smoke_test.go:144-158passes the variable only when a platform starts withwindows_, so the three Linux-only smoke tests pass nothing;swarm_addr_pool_test.go,upgrade_test.go,integration_test.goandtest_manager.goreference it zero times; and bothterraform.tfvars.templatefiles ship it commented out. Five call paths and both documented starting points would break. The conditional validation delivers the ruled intent — failure at plan time, naming the variable, for exactly the configuration measured — without that. The divergence is deliberate.The matching validator half lands in #682, which rejects a loaded config carrying an empty WinRM password. This PR stops that config being generated in the first place; #682 stops it being accepted. Neither is sufficient alone.
Cross-variable references in
validationrequire Terraform 1.9 or later. CI pins no version —hashicorp/setup-terraform@v4with noterraform_version— so it runs the current release. Recorded as a latent fragility, not addressed here.Upgrade hazard for existing stacks
The IAM role split destroys
common_role,common_profileand their policy attachments. The new profiles reach the launch templates only, and provision-aws does not refresh running instances on a launch-template change, so nodes provisioned before this change keep an instance profile whose role no longer exists and lose their CCM and EBS CSI permissions until they are replaced.aws-complete/iam.tfopens with that statement. Replace the nodes, or recreate the stack; do not re-apply in place against a live cluster. Enablinginstance_refreshis deliberately not done here — it would roll every node when an upstream AMI is republished, since the examples select AMIs withmost_recent = true— and is tracked as its own decision under PRODENG-4062.Testing
terraform fmt -check -recursive examples/terraform: clean.windows_passwordplan matrix againstaws-simple, countingInvalid value for variable: Linux-only unset 0; Linux-only withwindows_password = ""0; Windows nodegroup unset 1; Windows withwindows_password = ""1; Windows with a password 0.go test -v --tags 'testing' ./test: RC 0 —TestTerraformOptionsKeepsCredentialsOutOfTheCommandLine,TestSecretVarsRoundTripThroughTerraformandTestAdminCIDRsall pass.go vet ./test/...RC 0, sotest/smokeandtest/integrationstill compile.make lintRC 0, 0 issues. The full./test/...pattern is deliberately not run:test/smokeandtest/integrationhave aTestMainthat provisions real AWS stacks.An existing main-era
terraform.tfvars— reconstructed verbatim fromgit show origin/main:…/terraform.tfvars.template, with onlynameuncommented and the MKE password filled — now plans. Before/after, same file, both examples:The after-plan reaches resource generation (
+ createfortls_private_key.this,local_sensitive_file.ssh_private_key,time_static.now) and stops only at the AWS provider, which has no credentials here by design. Variable resolution and validation are past; that pair is the proof the breakage is gone.Credential emission verified by rendering, not only by reading: a throwaway module reproducing the heredoc with the same escaping emits
adminPassword: Sup3rS3cret!when the values come from a tfvars with nothing exported, andadminPassword: FromEnvP@sswhen they come fromTF_VAR_launchpadwith no-vararguments. Both routes produce a usable document.AC-4 is claimed at the plan level only: the suites supply
admin_cidrs. A smoke run was not executed — it needs live AWS and is gated — so it is not shown that a provisioned cluster comes up under the narrowed rules.windows_passwordvalidation exercised byterraform planagainstaws-simple(no AWS credentials, so the plan stops at the provider, after variable validation): a Linux-only config raises no variable error; a config whose nodegroup platform iswindows_2022and which passes no password fails withInvalid value for variable ... windows_password must be set to a non-empty value when any nodegroup uses a windows_ platform; the same config passingwindows_password = ""fails identically; a Linux-only config that does pass a password raises no error.aws-simple:terraform init+terraform validateon a scratch copy —Success! The configuration is valid.aws-completedoes not validate today, before this change. On agit archiveof untouchedmain@ce4a91d,terraform validatefails with:This is the helm provider v3 syntax change, pre-existing and out of scope here. With the helm provider pinned to
~> 2.17in the scratch copy only, validation of this branch succeeds.No apply, no AWS. Nothing was provisioned and no credential profile was used.
AC-4 is not demonstrated: it needs the held test-side change plus a smoke run. The argument available now is structural — which ports are reachable from which sources, and that the load-balanced ports retain a VPC-sourced rule. It does not cover a port MKE needs that nothing documents.
aws-completechanges carry more risk thanaws-simpleones for three independent reasons: no smoke suite exercises it, it does not currently validate at all, and its ed25519 key cannot decrypt EC2 password data. That is where review attention is worth spending.Aikido scanning is parked by user ruling and was not run; no other check here covers it.
Links
https://mirantis.jira.com/browse/PRODENG-3929 — findings
secf_1d9be1096e3191dee288119a,secf_59e5c583a76665a8ae369d7f,secf_1d7359175d2e08a1777bb478.This PR does not complete PRODENG-3929, to the same extent as before the 2026-11-06 rework — that rework changed the examples' input surface, not which acceptance criteria are met. AC-1 and AC-3 are implemented; AC-2 is partially implemented (
sensitive = true) with the remainder specified above pending a decision; AC-4's test-side companion has landed, demonstrated at plan level rather than by a smoke run. Nothing in PRODENG-3929's acceptance criteria requiredadmin_cidrsto be a required variable: the ticket asks for the ingress to be scoped, and it still is — by port in both examples, and by CIDR for any operator who narrows the default. What changed is the out-of-the-box posture, not the mechanism. The one item that genuinely goes out is the carried-in PRODENG-3935 finding, which is open again.Checklist
admin_cidrsdefaults to0.0.0.0/0by user ruling, documented as opensensitive = trueapplied; password-data path specified, not implementedaws-complete;aws-simpleexcluded with evidenceadmin_cidrs; not demonstrated by a smoke runWritten by AI: claude-opus-5