From aa9df3b884b865ec6c3dbd5feb4d732303b03c52 Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Fri, 10 Jul 2026 21:13:26 -0400 Subject: [PATCH 1/8] feat(azure): add optional Kali Linux attack box MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an opt-in Kali VM that deploys on its own subnet inside the lab VNet, accessible only via Azure Bastion SSH. Marketplace image, headless, no provisioning needed — stock Kali tooling for AD attack exercises. - New `terraform-azure-kali` module (subnet, NSG, NIC, VM with marketplace plan) - `--with-kali` flag on infra apply/plan/destroy - Destroy always includes kali to prevent orphaned VMs - `dreadgoad bastion ssh kali` auto-detects AttackBox role, user, and key - Doctor check warns if marketplace terms not accepted - `env create` auto-derives kali subnet CIDR from VNet - Fix: bastion PersistentPreRunE now calls config.Init() (pre-existing bug) Co-Authored-By: Claude --- cli/cmd/bastion.go | 69 ++++++-- cli/cmd/env_cmd.go | 42 +++-- cli/cmd/env_cmd_test.go | 24 +-- cli/cmd/infra_cmd.go | 13 ++ cli/internal/doctor/checks.go | 31 ++++ .../test/centralus/kali/terragrunt.hcl | 78 +++++++++ infra/azure/goad-deployment/test/env.hcl | 5 + modules/terraform-azure-kali/README.md | 67 ++++++++ modules/terraform-azure-kali/main.tf | 151 ++++++++++++++++++ modules/terraform-azure-kali/outputs.tf | 44 +++++ modules/terraform-azure-kali/variables.tf | 113 +++++++++++++ modules/terraform-azure-kali/versions.tf | 18 +++ 12 files changed, 616 insertions(+), 39 deletions(-) create mode 100644 infra/azure/goad-deployment/test/centralus/kali/terragrunt.hcl create mode 100644 modules/terraform-azure-kali/README.md create mode 100644 modules/terraform-azure-kali/main.tf create mode 100644 modules/terraform-azure-kali/outputs.tf create mode 100644 modules/terraform-azure-kali/variables.tf create mode 100644 modules/terraform-azure-kali/versions.tf diff --git a/cli/cmd/bastion.go b/cli/cmd/bastion.go index 8dd2f937..8b6e60af 100644 --- a/cli/cmd/bastion.go +++ b/cli/cmd/bastion.go @@ -29,6 +29,9 @@ before 'infra apply', or use 'infra apply --with-bastion'). Tunneling-enabled Standard/Premium SKUs are required for ssh/rdp/tunnel; the Developer SKU only supports the browser console.`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if err := config.Init(); err != nil { + return err + } cfg, err := config.Get() if err != nil { return err @@ -198,23 +201,37 @@ func runBastionSSH(cmd *cobra.Command, args []string) error { authType, _ := cmd.Flags().GetString("auth-type") sshKey, _ := cmd.Flags().GetString("ssh-key") - // Auto-pick the ephemeral key for the in-VNet Ansible controller. The - // terraform-azure-controller module writes its private key to a - // well-known path and stamps Role=AnsibleController on the VM, so we - // can reach it without making the operator type --auth-type ssh-key - // --ssh-key -u dreadadmin every time. A failed live lookup is - // non-fatal — we just fall back to the user-supplied flag values. - if inst, err := client.FindInstanceByHostname(ctx, cfg.Env, args[0]); err == nil && inst.Tags["Role"] == "AnsibleController" { - if !cmd.Flags().Changed("auth-type") { - authType = "ssh-key" - } - if !cmd.Flags().Changed("ssh-key") && authType == "ssh-key" { - if path := controllerKeyPath(cfg.Env, inst.Name); path != "" { - sshKey = path + // Auto-pick the ephemeral key for known VM roles. The terraform modules + // write private keys to well-known paths and stamp Role tags on the VMs, + // so we can reach them without making the operator type --auth-type + // ssh-key --ssh-key -u every time. A failed live lookup + // is non-fatal — we just fall back to the user-supplied flag values. + if inst, err := client.FindInstanceByHostname(ctx, cfg.Env, args[0]); err == nil { + switch inst.Tags["Role"] { + case "AnsibleController": + if !cmd.Flags().Changed("auth-type") { + authType = "ssh-key" + } + if !cmd.Flags().Changed("ssh-key") && authType == "ssh-key" { + if path := controllerKeyPath(cfg.Env, inst.Name); path != "" { + sshKey = path + } + } + if !cmd.Flags().Changed("user") { + user = "dreadadmin" + } + case "AttackBox": + if !cmd.Flags().Changed("auth-type") { + authType = "ssh-key" + } + if !cmd.Flags().Changed("ssh-key") && authType == "ssh-key" { + if path := kaliKeyPath(cfg.Env, inst.Name); path != "" { + sshKey = path + } + } + if !cmd.Flags().Changed("user") { + user = "kali" } - } - if !cmd.Flags().Changed("user") { - user = "dreadadmin" } } @@ -243,6 +260,26 @@ func controllerKeyPath(env, vmName string) string { return path } +// kaliKeyPath derives the conventional ephemeral private-key path the +// terraform-azure-kali module writes. VM names follow +// "{env}-{deployment}-kali-vm"; the module writes to +// "~/.dreadgoad/keys/azure-{env}-{deployment}-kali". +func kaliKeyPath(env, vmName string) string { + deployment := strings.TrimSuffix(strings.TrimPrefix(vmName, env+"-"), "-kali-vm") + if deployment == "" || deployment == vmName { + return "" + } + home, err := os.UserHomeDir() + if err != nil { + return "" + } + path := filepath.Join(home, ".dreadgoad", "keys", fmt.Sprintf("azure-%s-%s-kali", env, deployment)) + if _, err := os.Stat(path); err != nil { + return "" + } + return path +} + func runBastionRDP(cmd *cobra.Command, args []string) error { ctx := context.Background() client, host, cfg, err := bastionContext(ctx) diff --git a/cli/cmd/env_cmd.go b/cli/cmd/env_cmd.go index 321b3d20..baba713f 100644 --- a/cli/cmd/env_cmd.go +++ b/cli/cmd/env_cmd.go @@ -460,33 +460,43 @@ func generateVariantConfig(projectRoot, envName string) error { return gen.Run() } -// deriveAzureSubnets computes bastion and controller subnet CIDRs from a /16 -// VNet CIDR. Given "10.X.0.0/16" it produces: +// azureSubnets holds the computed subnet CIDRs for an Azure deployment. +type azureSubnets struct { + Bastion string + Controller string + Kali string +} + +// deriveAzureSubnets computes bastion, controller, and kali subnet CIDRs from +// a /16 VNet CIDR. Given "10.X.0.0/16" it produces: // // bastion: 10.X.2.0/26 (64 IPs, required by Azure Bastion) // controller: 10.X.3.0/28 (16 IPs, single Ansible controller) -func deriveAzureSubnets(vnetCIDR string) (bastionSubnet, controllerSubnet string, err error) { +// kali: 10.X.4.0/28 (16 IPs, optional attack box) +func deriveAzureSubnets(vnetCIDR string) (azureSubnets, error) { _, ipnet, err := net.ParseCIDR(vnetCIDR) if err != nil { - return "", "", fmt.Errorf("invalid VNet CIDR %q: %w", vnetCIDR, err) + return azureSubnets{}, fmt.Errorf("invalid VNet CIDR %q: %w", vnetCIDR, err) } ones, _ := ipnet.Mask.Size() if ones != 16 { - return "", "", fmt.Errorf("VNet CIDR must be a /16, got /%d", ones) + return azureSubnets{}, fmt.Errorf("VNet CIDR must be a /16, got /%d", ones) } base := ipnet.IP.To4() if base == nil { - return "", "", fmt.Errorf("VNet CIDR must be IPv4, got %q", vnetCIDR) + return azureSubnets{}, fmt.Errorf("VNet CIDR must be IPv4, got %q", vnetCIDR) } - bastionSubnet = fmt.Sprintf("%d.%d.2.0/26", base[0], base[1]) - controllerSubnet = fmt.Sprintf("%d.%d.3.0/28", base[0], base[1]) - return bastionSubnet, controllerSubnet, nil + return azureSubnets{ + Bastion: fmt.Sprintf("%d.%d.2.0/26", base[0], base[1]), + Controller: fmt.Sprintf("%d.%d.3.0/28", base[0], base[1]), + Kali: fmt.Sprintf("%d.%d.4.0/28", base[0], base[1]), + }, nil } -// createAzureEnvHCL writes an Azure-specific env.hcl with VNet, bastion, and -// controller subnet CIDRs auto-derived from the VNet CIDR. +// createAzureEnvHCL writes an Azure-specific env.hcl with VNet, bastion, +// controller, and kali subnet CIDRs auto-derived from the VNet CIDR. func createAzureEnvHCL(envDir, envName, vnetCIDR string) error { - bastionSubnet, controllerSubnet, err := deriveAzureSubnets(vnetCIDR) + subnets, err := deriveAzureSubnets(vnetCIDR) if err != nil { return err } @@ -505,8 +515,14 @@ func createAzureEnvHCL(envDir, envName, vnetCIDR string) error { controller_subnet_cidr = %q controller_ssh_source_address_prefix = %q controller_instance_size = "Standard_D2s_v3" + + # Optional Kali attack box. Enable with --with-kali on infra commands. + kali_subnet_cidr = %q + kali_ssh_source_address_prefix = %q + kali_instance_size = "Standard_D2s_v3" } -`, envName, vnetCIDR, bastionSubnet, controllerSubnet, bastionSubnet) +`, envName, vnetCIDR, subnets.Bastion, subnets.Controller, subnets.Bastion, + subnets.Kali, subnets.Bastion) return os.WriteFile(filepath.Join(envDir, "env.hcl"), []byte(content), 0o644) } diff --git a/cli/cmd/env_cmd_test.go b/cli/cmd/env_cmd_test.go index c8e0b28e..b20d0033 100644 --- a/cli/cmd/env_cmd_test.go +++ b/cli/cmd/env_cmd_test.go @@ -8,26 +8,30 @@ func TestDeriveAzureSubnets(t *testing.T) { vnetCIDR string wantBast string wantCtrl string + wantKali string wantErr bool }{ - {"standard", "10.8.0.0/16", "10.8.2.0/26", "10.8.3.0/28", false}, - {"different octet", "10.1.0.0/16", "10.1.2.0/26", "10.1.3.0/28", false}, - {"high octet", "10.200.0.0/16", "10.200.2.0/26", "10.200.3.0/28", false}, - {"not /16", "10.8.0.0/24", "", "", true}, - {"invalid CIDR", "not-a-cidr", "", "", true}, + {"standard", "10.8.0.0/16", "10.8.2.0/26", "10.8.3.0/28", "10.8.4.0/28", false}, + {"different octet", "10.1.0.0/16", "10.1.2.0/26", "10.1.3.0/28", "10.1.4.0/28", false}, + {"high octet", "10.200.0.0/16", "10.200.2.0/26", "10.200.3.0/28", "10.200.4.0/28", false}, + {"not /16", "10.8.0.0/24", "", "", "", true}, + {"invalid CIDR", "not-a-cidr", "", "", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bast, ctrl, err := deriveAzureSubnets(tt.vnetCIDR) + subnets, err := deriveAzureSubnets(tt.vnetCIDR) if (err != nil) != tt.wantErr { t.Fatalf("error = %v, wantErr %v", err, tt.wantErr) } - if bast != tt.wantBast { - t.Errorf("bastion = %q, want %q", bast, tt.wantBast) + if subnets.Bastion != tt.wantBast { + t.Errorf("bastion = %q, want %q", subnets.Bastion, tt.wantBast) } - if ctrl != tt.wantCtrl { - t.Errorf("controller = %q, want %q", ctrl, tt.wantCtrl) + if subnets.Controller != tt.wantCtrl { + t.Errorf("controller = %q, want %q", subnets.Controller, tt.wantCtrl) + } + if subnets.Kali != tt.wantKali { + t.Errorf("kali = %q, want %q", subnets.Kali, tt.wantKali) } }) } diff --git a/cli/cmd/infra_cmd.go b/cli/cmd/infra_cmd.go index 1d2844b9..ff5eade3 100644 --- a/cli/cmd/infra_cmd.go +++ b/cli/cmd/infra_cmd.go @@ -93,6 +93,7 @@ func init() { for _, cmd := range []*cobra.Command{infraApplyCmd, infraDestroyCmd, infraPlanCmd} { cmd.Flags().Bool("with-bastion", false, "(Azure) Include the optional Azure Bastion module") cmd.Flags().Bool("with-controller", false, "(Azure) Include the optional in-VNet Ansible controller module") + cmd.Flags().Bool("with-kali", false, "(Azure) Include the optional Kali Linux attack box") } infraCmd.PersistentFlags().StringP("deployment", "d", "", "Deployment name (default: from config)") @@ -179,6 +180,18 @@ func runInfraActionAzure(cmd *cobra.Command, cfg *config.Config, action string) if withController, _ := cmd.Flags().GetBool("with-controller"); withController { opts.ExtraEnv = append(opts.ExtraEnv, "DREADGOAD_ENABLE_AZURE_CONTROLLER=true") } + withKali, _ := cmd.Flags().GetBool("with-kali") + // On destroy, always include the kali module so orphaned VMs are cleaned up + // even if the user forgets --with-kali. + if !withKali && action == "destroy" { + kaliDir := filepath.Join(cfg.ProjectRoot, "infra", "azure", deployment, cfg.Env, region, "kali") + if _, err := os.Stat(kaliDir); err == nil { + withKali = true + } + } + if withKali { + opts.ExtraEnv = append(opts.ExtraEnv, "DREADGOAD_ENABLE_AZURE_KALI=true") + } if action == "apply" || action == "destroy" { autoApprove, _ := cmd.Flags().GetBool("auto-approve") diff --git a/cli/internal/doctor/checks.go b/cli/internal/doctor/checks.go index c9442c2e..3fe2f077 100644 --- a/cli/internal/doctor/checks.go +++ b/cli/internal/doctor/checks.go @@ -85,6 +85,7 @@ func runAzureChecks() []CheckResult { results = append(results, checkTerragrunt()) results = append(results, checkTerraformOrTofu()) results = append(results, checkPyPSRP()) + results = append(results, checkKaliMarketplaceTerms()) return results } @@ -409,6 +410,36 @@ func checkPyPSRP() CheckResult { return CheckResult{Name: "pypsrp + PySocks", Status: "pass", Message: "installed"} } +func checkKaliMarketplaceTerms() CheckResult { + out, err := exec.Command( + "az", "vm", "image", "terms", "show", + "--publisher", "kali-linux", "--offer", "kali", "--plan", "kali-2026-2", + "-o", "tsv", "--query", "accepted", + ).CombinedOutput() + if err != nil { + return CheckResult{ + Name: "Kali marketplace terms", + Status: "warn", + Message: "could not check (run: az vm image terms accept " + + "--publisher kali-linux --offer kali --plan kali-2026-2). " + + "Required if using --with-kali", + } + } + if strings.TrimSpace(string(out)) == "true" { + return CheckResult{ + Name: "Kali marketplace terms", + Status: "pass", + Message: "accepted", + } + } + return CheckResult{ + Name: "Kali marketplace terms", + Status: "warn", + Message: "not accepted. Required if using --with-kali. Run: " + + "az vm image terms accept --publisher kali-linux --offer kali --plan kali-2026-2", + } +} + func checkLudusSSH(opts LudusOptions) []CheckResult { var results []CheckResult diff --git a/infra/azure/goad-deployment/test/centralus/kali/terragrunt.hcl b/infra/azure/goad-deployment/test/centralus/kali/terragrunt.hcl new file mode 100644 index 00000000..bdc0ac73 --- /dev/null +++ b/infra/azure/goad-deployment/test/centralus/kali/terragrunt.hcl @@ -0,0 +1,78 @@ +# ============================================================================= +# Optional Kali Linux Attack Box +# +# Deploys a headless Kali VM on its own subnet for attacking the lab from +# inside the VNet. Access is via Azure Bastion SSH — no public IP. +# +# Enable by setting DREADGOAD_ENABLE_AZURE_KALI=true before Terragrunt runs, +# or use `dreadgoad infra apply --with-kali`. Requires marketplace terms to be +# accepted: az vm image terms accept --publisher kali-linux --offer kali --plan kali-2026-2 +# ============================================================================= + +exclude { + if = lower(get_env("DREADGOAD_ENABLE_AZURE_KALI", "false")) != "true" + actions = ["all"] +} + +locals { + env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl")) + region_vars = read_terragrunt_config(find_in_parent_folders("region.hcl")) + + env = local.env_vars.locals.env + deployment_name = local.env_vars.locals.deployment_name + location = local.region_vars.locals.location + + kali_subnet_cidr = local.env_vars.locals.kali_subnet_cidr + kali_ssh_source_address_prefix = local.env_vars.locals.kali_ssh_source_address_prefix + kali_instance_size = local.env_vars.locals.kali_instance_size + + # SSH key resolution — same 3-tier pattern as the controller module. + ssh_key_inline = get_env("DREADGOAD_AZURE_KALI_SSH_KEY", "") + ssh_key_path_var = get_env("DREADGOAD_AZURE_KALI_SSH_KEY_PATH", "") + ssh_key_from_path = local.ssh_key_path_var != "" && fileexists(local.ssh_key_path_var) ? trimspace(file(local.ssh_key_path_var)) : "" + admin_ssh_public_key = ( + local.ssh_key_inline != "" ? local.ssh_key_inline : + local.ssh_key_from_path != "" ? local.ssh_key_from_path : + null + ) + + ephemeral_key_path = pathexpand("~/.dreadgoad/keys/azure-${local.env}-${local.deployment_name}-kali") +} + +terraform { + source = "${get_repo_root()}/modules//terraform-azure-kali" +} + +dependency "network" { + config_path = "../network" + mock_outputs = { + resource_group_name = "mock-rg" + location = "centralus" + vnet_name = "mock-vnet" + } + mock_outputs_allowed_terraform_commands = ["init", "validate", "plan"] +} + +include "root" { + path = find_in_parent_folders("root.hcl") +} + +inputs = { + env = local.env + deployment_name = local.deployment_name + location = dependency.network.outputs.location + resource_group_name = dependency.network.outputs.resource_group_name + virtual_network_name = dependency.network.outputs.vnet_name + + kali_subnet_cidr = local.kali_subnet_cidr + ssh_source_address_prefix = local.kali_ssh_source_address_prefix + instance_size = local.kali_instance_size + + admin_ssh_public_key = local.admin_ssh_public_key + ephemeral_key_output_path = local.ephemeral_key_path + + additional_tags = { + Project = "DreadGOAD" + Lab = "${local.deployment_name}-goad" + } +} diff --git a/infra/azure/goad-deployment/test/env.hcl b/infra/azure/goad-deployment/test/env.hcl index ba7073b4..b6a3cb4b 100644 --- a/infra/azure/goad-deployment/test/env.hcl +++ b/infra/azure/goad-deployment/test/env.hcl @@ -17,4 +17,9 @@ locals { # the GOAD DCs run on, so capacity is proven. Cost trade is small for a # transient lab. controller_instance_size = "Standard_D2s_v3" + + # Optional Kali attack box. Enable with --with-kali on infra commands. + kali_subnet_cidr = "10.8.4.0/28" + kali_ssh_source_address_prefix = "10.8.2.0/26" + kali_instance_size = "Standard_D2s_v3" } diff --git a/modules/terraform-azure-kali/README.md b/modules/terraform-azure-kali/README.md new file mode 100644 index 00000000..84c6b3a8 --- /dev/null +++ b/modules/terraform-azure-kali/README.md @@ -0,0 +1,67 @@ +## Requirements + +| Name | Version | +| ---- | ------- | +| [terraform](#requirement\_terraform) | >= 1.7 | +| [azurerm](#requirement\_azurerm) | ~> 4.0 | +| [local](#requirement\_local) | ~> 2.5 | +| [tls](#requirement\_tls) | ~> 4.0 | + +## Providers + +| Name | Version | +| ---- | ------- | +| [azurerm](#provider\_azurerm) | ~> 4.0 | +| [local](#provider\_local) | ~> 2.5 | +| [tls](#provider\_tls) | ~> 4.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +| ---- | ---- | +| [azurerm_linux_virtual_machine.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_virtual_machine) | resource | +| [azurerm_network_interface.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_interface) | resource | +| [azurerm_network_security_group.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_group) | resource | +| [azurerm_subnet.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet) | resource | +| [azurerm_subnet_network_security_group_association.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet_network_security_group_association) | resource | +| [local_sensitive_file.kali_key](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/sensitive_file) | resource | +| [tls_private_key.kali](https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/private_key) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +| ---- | ----------- | ---- | ------- | :------: | +| [additional\_tags](#input\_additional\_tags) | Tags applied to every resource. | `map(string)` | `{}` | no | +| [admin\_ssh\_public\_key](#input\_admin\_ssh\_public\_key) | SSH public key authorised on the Kali VM. When null, the module generates an ephemeral ed25519 keypair and writes the private key to ephemeral\_key\_output\_path. | `string` | `null` | no | +| [admin\_username](#input\_admin\_username) | Local admin username for the Kali VM. | `string` | `"kali"` | no | +| [deployment\_name](#input\_deployment\_name) | Name of the deployment (e.g. "goad"). | `string` | n/a | yes | +| [env](#input\_env) | Environment name (e.g. test, staging). | `string` | n/a | yes | +| [ephemeral\_key\_output\_path](#input\_ephemeral\_key\_output\_path) | Filesystem path to write the generated private key when admin\_ssh\_public\_key is null. | `string` | `null` | no | +| [instance\_size](#input\_instance\_size) | Azure VM size. B2s (2 vCPU, 4 GB) is sufficient for CLI-based AD attack tooling. | `string` | `"Standard_B2s"` | no | +| [kali\_subnet\_cidr](#input\_kali\_subnet\_cidr) | CIDR for the Kali attack box's dedicated subnet. /28 is plenty for one VM. | `string` | `"10.8.4.0/28"` | no | +| [location](#input\_location) | Azure region. | `string` | n/a | yes | +| [os\_disk\_size\_gb](#input\_os\_disk\_size\_gb) | Size of the OS disk in GB. 32 is enough for stock Kali tooling. | `number` | `32` | no | +| [os\_disk\_storage\_account\_type](#input\_os\_disk\_storage\_account\_type) | Storage account type for the OS disk. | `string` | `"StandardSSD_LRS"` | no | +| [plan](#input\_plan) | Marketplace plan terms for the Kali image. Must be accepted per-subscription via `az vm image terms accept --publisher kali-linux --offer kali --plan `. |
object({
name = string
product = string
publisher = string
})
|
{
"name": "kali-2026-2",
"product": "kali",
"publisher": "kali-linux"
}
| no | +| [resource\_group\_name](#input\_resource\_group\_name) | Resource group the Kali VM and its NIC/NSG/subnet are deployed into. | `string` | n/a | yes | +| [source\_image](#input\_source\_image) | Marketplace image reference. Defaults to the latest Kali Linux release. |
object({
publisher = string
offer = string
sku = string
version = string
})
|
{
"offer": "kali",
"publisher": "kali-linux",
"sku": "kali-2026-2",
"version": "latest"
}
| no | +| [ssh\_source\_address\_prefix](#input\_ssh\_source\_address\_prefix) | Source allowed to reach the Kali box on TCP 22. Defaults to the AzureBastionSubnet CIDR. | `string` | `"10.8.2.0/26"` | no | +| [virtual\_network\_name](#input\_virtual\_network\_name) | VNet name where the Kali subnet will be created. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +| ---- | ----------- | +| [admin\_username](#output\_admin\_username) | Local admin username for the Kali VM. | +| [computer\_name](#output\_computer\_name) | Linux hostname assigned to the Kali attack box. | +| [nsg\_id](#output\_nsg\_id) | NSG ID gating the Kali subnet. | +| [private\_ip](#output\_private\_ip) | Private IP address of the Kali VM's NIC. | +| [ssh\_private\_key\_path](#output\_ssh\_private\_key\_path) | Filesystem path to the generated private key when the module created an ephemeral keypair; null when an explicit admin\_ssh\_public\_key was supplied. | +| [ssh\_public\_key\_openssh](#output\_ssh\_public\_key\_openssh) | OpenSSH-formatted public key authorised on the Kali VM. | +| [subnet\_id](#output\_subnet\_id) | Subnet ID created for the Kali attack box. | +| [vm\_id](#output\_vm\_id) | Azure VM resource ID for the Kali attack box. | +| [vm\_name](#output\_vm\_name) | Azure VM resource name for the Kali attack box. | diff --git a/modules/terraform-azure-kali/main.tf b/modules/terraform-azure-kali/main.tf new file mode 100644 index 00000000..0147e59b --- /dev/null +++ b/modules/terraform-azure-kali/main.tf @@ -0,0 +1,151 @@ +locals { + name_prefix = "${var.env}-${var.deployment_name}-kali" + + base_tags = { + Module = "terraform-azure-kali" + Environment = var.env + ManagedBy = "Terraform" + AccessMethod = "BastionSSH" + Role = "AttackBox" + } + + tags = merge(local.base_tags, var.additional_tags) + + generate_ssh_key = var.admin_ssh_public_key == null + + effective_public_key = ( + local.generate_ssh_key + ? tls_private_key.kali[0].public_key_openssh + : var.admin_ssh_public_key + ) +} + +resource "tls_private_key" "kali" { + count = local.generate_ssh_key ? 1 : 0 + + algorithm = "ED25519" +} + +resource "local_sensitive_file" "kali_key" { + count = local.generate_ssh_key ? 1 : 0 + + content = tls_private_key.kali[0].private_key_openssh + filename = var.ephemeral_key_output_path + file_permission = "0600" + + lifecycle { + precondition { + condition = var.ephemeral_key_output_path != null + error_message = "ephemeral_key_output_path must be set when admin_ssh_public_key is null." + } + } +} + +resource "azurerm_subnet" "this" { + name = "${local.name_prefix}-subnet" + resource_group_name = var.resource_group_name + virtual_network_name = var.virtual_network_name + address_prefixes = [var.kali_subnet_cidr] +} + +resource "azurerm_network_security_group" "this" { + name = "${local.name_prefix}-nsg" + location = var.location + resource_group_name = var.resource_group_name + + security_rule { + name = "AllowSSHFromBastion" + priority = 100 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "22" + source_address_prefix = var.ssh_source_address_prefix + destination_address_prefix = "*" + } + + security_rule { + name = "AllowAzureLoadBalancer" + priority = 110 + direction = "Inbound" + access = "Allow" + protocol = "*" + source_port_range = "*" + destination_port_range = "*" + source_address_prefix = "AzureLoadBalancer" + destination_address_prefix = "*" + } + + security_rule { + name = "DenyAllInbound" + priority = 4096 + direction = "Inbound" + access = "Deny" + protocol = "*" + source_port_range = "*" + destination_port_range = "*" + source_address_prefix = "*" + destination_address_prefix = "*" + } + + tags = merge(local.tags, { Name = "${local.name_prefix}-nsg" }) +} + +resource "azurerm_subnet_network_security_group_association" "this" { + subnet_id = azurerm_subnet.this.id + network_security_group_id = azurerm_network_security_group.this.id +} + +resource "azurerm_network_interface" "this" { + name = "${local.name_prefix}-nic" + location = var.location + resource_group_name = var.resource_group_name + + ip_configuration { + name = "internal" + subnet_id = azurerm_subnet.this.id + private_ip_address_allocation = "Dynamic" + } + + tags = merge(local.tags, { Name = "${local.name_prefix}-nic" }) +} + +resource "azurerm_linux_virtual_machine" "this" { + name = "${local.name_prefix}-vm" + computer_name = substr(local.name_prefix, 0, 63) + resource_group_name = var.resource_group_name + location = var.location + size = var.instance_size + admin_username = var.admin_username + + network_interface_ids = [azurerm_network_interface.this.id] + + disable_password_authentication = true + + admin_ssh_key { + username = var.admin_username + public_key = local.effective_public_key + } + + os_disk { + caching = "ReadWrite" + storage_account_type = var.os_disk_storage_account_type + disk_size_gb = var.os_disk_size_gb + } + + source_image_reference { + publisher = var.source_image.publisher + offer = var.source_image.offer + sku = var.source_image.sku + version = var.source_image.version + } + + plan { + name = var.plan.name + product = var.plan.product + publisher = var.plan.publisher + } + + tags = merge(local.tags, { Name = "${local.name_prefix}-vm" }) +} diff --git a/modules/terraform-azure-kali/outputs.tf b/modules/terraform-azure-kali/outputs.tf new file mode 100644 index 00000000..b84a38c4 --- /dev/null +++ b/modules/terraform-azure-kali/outputs.tf @@ -0,0 +1,44 @@ +output "vm_id" { + description = "Azure VM resource ID for the Kali attack box." + value = azurerm_linux_virtual_machine.this.id +} + +output "vm_name" { + description = "Azure VM resource name for the Kali attack box." + value = azurerm_linux_virtual_machine.this.name +} + +output "computer_name" { + description = "Linux hostname assigned to the Kali attack box." + value = azurerm_linux_virtual_machine.this.computer_name +} + +output "private_ip" { + description = "Private IP address of the Kali VM's NIC." + value = azurerm_network_interface.this.private_ip_address +} + +output "subnet_id" { + description = "Subnet ID created for the Kali attack box." + value = azurerm_subnet.this.id +} + +output "nsg_id" { + description = "NSG ID gating the Kali subnet." + value = azurerm_network_security_group.this.id +} + +output "admin_username" { + description = "Local admin username for the Kali VM." + value = var.admin_username +} + +output "ssh_private_key_path" { + description = "Filesystem path to the generated private key when the module created an ephemeral keypair; null when an explicit admin_ssh_public_key was supplied." + value = local.generate_ssh_key ? var.ephemeral_key_output_path : null +} + +output "ssh_public_key_openssh" { + description = "OpenSSH-formatted public key authorised on the Kali VM." + value = local.effective_public_key +} diff --git a/modules/terraform-azure-kali/variables.tf b/modules/terraform-azure-kali/variables.tf new file mode 100644 index 00000000..4c00aec3 --- /dev/null +++ b/modules/terraform-azure-kali/variables.tf @@ -0,0 +1,113 @@ +variable "deployment_name" { + description = "Name of the deployment (e.g. \"goad\")." + type = string +} + +variable "env" { + description = "Environment name (e.g. test, staging)." + type = string +} + +variable "location" { + description = "Azure region." + type = string +} + +variable "resource_group_name" { + description = "Resource group the Kali VM and its NIC/NSG/subnet are deployed into." + type = string +} + +variable "virtual_network_name" { + description = "VNet name where the Kali subnet will be created." + type = string +} + +variable "kali_subnet_cidr" { + description = "CIDR for the Kali attack box's dedicated subnet. /28 is plenty for one VM." + type = string + default = "10.8.4.0/28" + + validation { + condition = can(cidrhost(var.kali_subnet_cidr, 0)) + error_message = "kali_subnet_cidr must be a valid IPv4 CIDR block." + } +} + +variable "ssh_source_address_prefix" { + description = "Source allowed to reach the Kali box on TCP 22. Defaults to the AzureBastionSubnet CIDR." + type = string + default = "10.8.2.0/26" +} + +variable "instance_size" { + description = "Azure VM size. D2s_v3 (2 vCPU, 8 GB) handles concurrent attack tooling comfortably." + type = string + default = "Standard_D2s_v3" +} + +variable "admin_username" { + description = "Local admin username for the Kali VM." + type = string + default = "kali" +} + +variable "admin_ssh_public_key" { + description = "SSH public key authorised on the Kali VM. When null, the module generates an ephemeral ed25519 keypair and writes the private key to ephemeral_key_output_path." + type = string + default = null +} + +variable "ephemeral_key_output_path" { + description = "Filesystem path to write the generated private key when admin_ssh_public_key is null." + type = string + default = null +} + +variable "os_disk_size_gb" { + description = "Size of the OS disk in GB. 32 is enough for stock Kali tooling." + type = number + default = 32 +} + +variable "os_disk_storage_account_type" { + description = "Storage account type for the OS disk." + type = string + default = "StandardSSD_LRS" +} + +variable "source_image" { + description = "Marketplace image reference. Defaults to the latest Kali Linux release." + type = object({ + publisher = string + offer = string + sku = string + version = string + }) + default = { + publisher = "kali-linux" + offer = "kali" + sku = "kali-2026-2" + version = "latest" + } +} + +variable "plan" { + description = "Marketplace plan terms for the Kali image. Must be accepted per-subscription via `az vm image terms accept --publisher kali-linux --offer kali --plan `." + type = object({ + name = string + product = string + publisher = string + }) + default = { + name = "kali-2026-2" + product = "kali" + publisher = "kali-linux" + } +} + +variable "additional_tags" { + description = "Tags applied to every resource." + type = map(string) + default = {} +} diff --git a/modules/terraform-azure-kali/versions.tf b/modules/terraform-azure-kali/versions.tf new file mode 100644 index 00000000..3c3d705b --- /dev/null +++ b/modules/terraform-azure-kali/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.7" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.0" + } + tls = { + source = "hashicorp/tls" + version = "~> 4.0" + } + local = { + source = "hashicorp/local" + version = "~> 2.5" + } + } +} From 3f6ce1ca930a2a41cded62d52f15f4cf67f3e6b0 Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Sun, 12 Jul 2026 13:57:18 -0400 Subject: [PATCH 2/8] chore: regenerate terraform-azure-kali docs after instance_size change Co-Authored-By: Claude --- modules/terraform-azure-kali/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/terraform-azure-kali/README.md b/modules/terraform-azure-kali/README.md index 84c6b3a8..072c3e10 100644 --- a/modules/terraform-azure-kali/README.md +++ b/modules/terraform-azure-kali/README.md @@ -11,9 +11,9 @@ | Name | Version | | ---- | ------- | -| [azurerm](#provider\_azurerm) | ~> 4.0 | -| [local](#provider\_local) | ~> 2.5 | -| [tls](#provider\_tls) | ~> 4.0 | +| [azurerm](#provider\_azurerm) | 4.80.0 | +| [local](#provider\_local) | 2.9.0 | +| [tls](#provider\_tls) | 4.3.0 | ## Modules @@ -41,7 +41,7 @@ No modules. | [deployment\_name](#input\_deployment\_name) | Name of the deployment (e.g. "goad"). | `string` | n/a | yes | | [env](#input\_env) | Environment name (e.g. test, staging). | `string` | n/a | yes | | [ephemeral\_key\_output\_path](#input\_ephemeral\_key\_output\_path) | Filesystem path to write the generated private key when admin\_ssh\_public\_key is null. | `string` | `null` | no | -| [instance\_size](#input\_instance\_size) | Azure VM size. B2s (2 vCPU, 4 GB) is sufficient for CLI-based AD attack tooling. | `string` | `"Standard_B2s"` | no | +| [instance\_size](#input\_instance\_size) | Azure VM size. D2s\_v3 (2 vCPU, 8 GB) handles concurrent attack tooling comfortably. | `string` | `"Standard_D2s_v3"` | no | | [kali\_subnet\_cidr](#input\_kali\_subnet\_cidr) | CIDR for the Kali attack box's dedicated subnet. /28 is plenty for one VM. | `string` | `"10.8.4.0/28"` | no | | [location](#input\_location) | Azure region. | `string` | n/a | yes | | [os\_disk\_size\_gb](#input\_os\_disk\_size\_gb) | Size of the OS disk in GB. 32 is enough for stock Kali tooling. | `number` | `32` | no | From e1b62a7eb762286980d05595fd88f7f96d102826 Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Sun, 12 Jul 2026 14:07:05 -0400 Subject: [PATCH 3/8] fix: clarify ephemeral_key_output_path is conditionally required Match the controller module's description pattern. Co-Authored-By: Claude --- modules/terraform-azure-kali/README.md | 2 +- modules/terraform-azure-kali/variables.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/terraform-azure-kali/README.md b/modules/terraform-azure-kali/README.md index 072c3e10..6297ce68 100644 --- a/modules/terraform-azure-kali/README.md +++ b/modules/terraform-azure-kali/README.md @@ -40,7 +40,7 @@ No modules. | [admin\_username](#input\_admin\_username) | Local admin username for the Kali VM. | `string` | `"kali"` | no | | [deployment\_name](#input\_deployment\_name) | Name of the deployment (e.g. "goad"). | `string` | n/a | yes | | [env](#input\_env) | Environment name (e.g. test, staging). | `string` | n/a | yes | -| [ephemeral\_key\_output\_path](#input\_ephemeral\_key\_output\_path) | Filesystem path to write the generated private key when admin\_ssh\_public\_key is null. | `string` | `null` | no | +| [ephemeral\_key\_output\_path](#input\_ephemeral\_key\_output\_path) | Filesystem path to write the generated private key when admin\_ssh\_public\_key is null. Required in that case; ignored when an explicit public key is supplied. | `string` | `null` | no | | [instance\_size](#input\_instance\_size) | Azure VM size. D2s\_v3 (2 vCPU, 8 GB) handles concurrent attack tooling comfortably. | `string` | `"Standard_D2s_v3"` | no | | [kali\_subnet\_cidr](#input\_kali\_subnet\_cidr) | CIDR for the Kali attack box's dedicated subnet. /28 is plenty for one VM. | `string` | `"10.8.4.0/28"` | no | | [location](#input\_location) | Azure region. | `string` | n/a | yes | diff --git a/modules/terraform-azure-kali/variables.tf b/modules/terraform-azure-kali/variables.tf index 4c00aec3..10c74174 100644 --- a/modules/terraform-azure-kali/variables.tf +++ b/modules/terraform-azure-kali/variables.tf @@ -59,7 +59,7 @@ variable "admin_ssh_public_key" { } variable "ephemeral_key_output_path" { - description = "Filesystem path to write the generated private key when admin_ssh_public_key is null." + description = "Filesystem path to write the generated private key when admin_ssh_public_key is null. Required in that case; ignored when an explicit public key is supplied." type = string default = null } From 19f42a421490b6549bb55d4cd10aeb5fcac1ff39 Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Sun, 12 Jul 2026 14:44:17 -0400 Subject: [PATCH 4/8] fix: reduce runBastionSSH complexity and add terraform-docs markers Extract role-based SSH defaults into resolveRoleDefaults() to bring runBastionSSH below the gocyclo threshold of 15. Add BEGIN/END_TF_DOCS markers to the kali README to match the controller module pattern. Co-Authored-By: Claude --- cli/cmd/bastion.go | 72 +++++++++++++++----------- modules/terraform-azure-kali/README.md | 4 ++ 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/cli/cmd/bastion.go b/cli/cmd/bastion.go index 8b6e60af..13f90cf5 100644 --- a/cli/cmd/bastion.go +++ b/cli/cmd/bastion.go @@ -201,37 +201,17 @@ func runBastionSSH(cmd *cobra.Command, args []string) error { authType, _ := cmd.Flags().GetString("auth-type") sshKey, _ := cmd.Flags().GetString("ssh-key") - // Auto-pick the ephemeral key for known VM roles. The terraform modules - // write private keys to well-known paths and stamp Role tags on the VMs, - // so we can reach them without making the operator type --auth-type - // ssh-key --ssh-key -u every time. A failed live lookup + // Auto-pick the ephemeral key for known VM roles. A failed live lookup // is non-fatal — we just fall back to the user-supplied flag values. - if inst, err := client.FindInstanceByHostname(ctx, cfg.Env, args[0]); err == nil { - switch inst.Tags["Role"] { - case "AnsibleController": - if !cmd.Flags().Changed("auth-type") { - authType = "ssh-key" - } - if !cmd.Flags().Changed("ssh-key") && authType == "ssh-key" { - if path := controllerKeyPath(cfg.Env, inst.Name); path != "" { - sshKey = path - } - } - if !cmd.Flags().Changed("user") { - user = "dreadadmin" - } - case "AttackBox": - if !cmd.Flags().Changed("auth-type") { - authType = "ssh-key" - } - if !cmd.Flags().Changed("ssh-key") && authType == "ssh-key" { - if path := kaliKeyPath(cfg.Env, inst.Name); path != "" { - sshKey = path - } - } - if !cmd.Flags().Changed("user") { - user = "kali" - } + if defaults := resolveRoleDefaults(client, ctx, cfg.Env, args[0]); defaults != nil { + if !cmd.Flags().Changed("auth-type") { + authType = defaults.authType + } + if !cmd.Flags().Changed("ssh-key") && authType == "ssh-key" && defaults.sshKey != "" { + sshKey = defaults.sshKey + } + if !cmd.Flags().Changed("user") { + user = defaults.user } } @@ -239,6 +219,38 @@ func runBastionSSH(cmd *cobra.Command, args []string) error { return client.OpenBastionSSH(ctx, host, vmID, user, authType, sshKey) } +// roleDefaults holds auto-detected SSH defaults for a known VM role. +type roleDefaults struct { + authType string + user string + sshKey string +} + +// resolveRoleDefaults looks up a VM by hostname and returns SSH defaults based +// on its Role tag. Returns nil if the VM is not found or has no known role. +func resolveRoleDefaults(client *azure.Client, ctx context.Context, env, hostname string) *roleDefaults { + inst, err := client.FindInstanceByHostname(ctx, env, hostname) + if err != nil { + return nil + } + switch inst.Tags["Role"] { + case "AnsibleController": + return &roleDefaults{ + authType: "ssh-key", + user: "dreadadmin", + sshKey: controllerKeyPath(env, inst.Name), + } + case "AttackBox": + return &roleDefaults{ + authType: "ssh-key", + user: "kali", + sshKey: kaliKeyPath(env, inst.Name), + } + default: + return nil + } +} + // controllerKeyPath derives the conventional ephemeral private-key path the // terraform-azure-controller module writes. VM names follow // "{env}-{deployment}-controller-vm"; the module writes to diff --git a/modules/terraform-azure-kali/README.md b/modules/terraform-azure-kali/README.md index 6297ce68..74815ac7 100644 --- a/modules/terraform-azure-kali/README.md +++ b/modules/terraform-azure-kali/README.md @@ -1,3 +1,6 @@ +# terraform-azure-kali + + ## Requirements | Name | Version | @@ -65,3 +68,4 @@ No modules. | [subnet\_id](#output\_subnet\_id) | Subnet ID created for the Kali attack box. | | [vm\_id](#output\_vm\_id) | Azure VM resource ID for the Kali attack box. | | [vm\_name](#output\_vm\_name) | Azure VM resource name for the Kali attack box. | + From 18af48d24daa6c195e8c9a97fbcc5f3f03ecfa11 Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Sun, 12 Jul 2026 14:56:30 -0400 Subject: [PATCH 5/8] chore: commit terraform-azure-kali lock file Match the pattern of other Azure modules that track their lock files. Co-Authored-By: Claude --- .../terraform-azure-kali/.terraform.lock.hcl | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 modules/terraform-azure-kali/.terraform.lock.hcl diff --git a/modules/terraform-azure-kali/.terraform.lock.hcl b/modules/terraform-azure-kali/.terraform.lock.hcl new file mode 100644 index 00000000..ed3721d5 --- /dev/null +++ b/modules/terraform-azure-kali/.terraform.lock.hcl @@ -0,0 +1,113 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/azurerm" { + version = "4.80.0" + constraints = "~> 4.0" + hashes = [ + "h1:/3VCHqW2jY/9fum11q23cxIeVLBi3nm5cufGvFN7pP4=", + "h1:3ugN9+6K4hJXpVvISHpyEfit+nj6KDH9WmYU01uIfsw=", + "h1:4WDTvTdpSZLSYuwSNfDS7qcCchUUYHec027bF41gb7s=", + "h1:9cgluGNXUB/ILYC2xHoop97QqqVkLkEzUe5yoQrllP4=", + "h1:BkDHI0+fPlHKM9PIOKnOwGUxgYOV65O2P4cWebI3aB0=", + "h1:C9yMt4aisAy2Tc4RYqezUHaho8yUB3Cyqg1Beach7bY=", + "h1:I8uN35ybRHD0rmvjE+lh+bCFLM6vM1p8QLm2RIsn20Q=", + "h1:KbYTNYdayAxMb8aEp767242j0/JU3/ri2XH05eURCFY=", + "h1:MNvJ+4ccdPV0bIKLimKkmvUAuuCdx2a1MoR72vjABsI=", + "h1:WZuPh5xdWlnHlKH8yWrG+7ZkL2nvooKGscBJAKA5fv8=", + "h1:bpmNZO9WMHOOknKjZQEV72Rwjjfiks049RCP1gKTaoI=", + "h1:eAy7agtpfkC+6eHb7NmtTNXcDd67qvtfbRFvLCD0n6w=", + "h1:p0/RKU7/erfKriGOjgQBAQHjJkX2oWfvAri+euYPV9A=", + "h1:p6UGxTkUbB2OjomDXN/6PGZAxfxKoK1BhqOLZaBl3YA=", + "h1:suntvZgX/12BEoFZzn3WVAWzPzm/41urdh1zl1DkfcE=", + "zh:0737e143b21aed8a9dd8230c913cb653a127a52f335dc52f1d212593503117dc", + "zh:0c0262a7cda79d3e850545a74fe3151dd00e2b0a9923cfdb101fe05c1271404f", + "zh:211586e6317b9c432d1d1dfa2ddb72f9a25704088b5c77ce7b78eede94eb3209", + "zh:2eb97f4fdc5dd2550d300403ba5c8a84a40c776a957126cdda4c7a3fca11bdda", + "zh:3218e942b8417775d45ba4d23dc777649c0a5bbe5d2848a53b90c1ba494660cc", + "zh:38a747101fa3a95ebd0390b8382d02ed016ea0cfba61b9b9a8f0b2ff2b8ae31d", + "zh:4151feabbb9e15b8dc5d7e4b2258ed4a4d2ff0f467fdfe7c805075235da3b92a", + "zh:5a589ac3bd83b0db1db60ff1001baea94c1d35be8fe2c4526ba8224d06769dc9", + "zh:753ef483a3af84d23750556aa8ba87cdb98ea57bf148c4e7a65dc6be7c7f3f5c", + "zh:a8e9273362cc670ec44769b71e32e128fcb25cfeb180903673bcb19a020aa73d", + "zh:bbd6f1099e386c987d0d0f16428dcde5f6c50e94d6a65c98648441f357d608d1", + "zh:d43989efee459f4dc43267fbc98f3ee8d8d6066c829db1627d305d633019d538", + "zh:d9036449c588ad700df2f254a69d828098b44d55ffb8ec52c31b27b0ab21d871", + "zh:e9e116793807c0af42de1d16ebb1b8c3bf0252f1eb9645f7662102fa80d8f8b0", + "zh:ec0e7e9a066f2a9f806afa2aa96fa21e2995ee85cfb40dbf96179dbe8e19f790", + ] +} + +provider "registry.opentofu.org/hashicorp/local" { + version = "2.9.0" + constraints = "~> 2.5" + hashes = [ + "h1:1dtKYW/5a1qob3yneL6WzOlnSGfYtJ6a2XeejCk9yb4=", + "h1:5NseXq5wU8O20ersTtV4ocrLYFFtgFr7n0pRLO1W2Rw=", + "h1:5d22ZPPK4iiygPbwRz/PJF5Es/0axVpMlPRpCR0Padw=", + "h1:AnwyolirmIlBMjH6+tV8bKkvT+5axJNYxi2y2IguiX4=", + "h1:PBp+HeseY021Fw3sLznCG27idgwPoff4cBuNmKgPL2w=", + "h1:VDxIhe4GbzdOCdmt7mQaqdwERQW6GSI7Roonts42Gr0=", + "h1:ZO6eWWnf8LjjV1q/JNeL9WLtZ6fwIttOnyN5LjCNSEo=", + "h1:dPIAf8oUAz+vW2E0iZunMvpuPddRZIztRsPSY1u+VnY=", + "h1:fwTDVG9AhFVKQZIb1EXkHv4FqzsZNlLWgkyPGDmZZEE=", + "h1:kDc465XPC7/6XFCjrMC4mTqhA9ef0FHKuJ3ZgfGNfeg=", + "h1:kGbjxrI2P8MHeyVtE1U3Q1TbyF71ExnHxtkrE+Aj6UU=", + "h1:kcoK6Afbsj54u9zaEqpecWAFKytqjBijtguCNwV3d4M=", + "h1:rxomJjDwOo+YZ+WIPc25FqEgsz9orh/2MCyUcZmFjvw=", + "h1:t0CMn/Rkwquw8l2yQ+O4ApzbMZfY2UazbsDnZygzACA=", + "h1:tJwgm2BS4xCGlElCDQEFXQoefY9Y4t0JdSKTtsPBbBo=", + "zh:13ef7ecd1e397ec5b20ea588508dd3e3b8d6c50d809ae76b079abf9dd8d02e4b", + "zh:2190c9325980076489ce02b0f5dd2c0b91fc8711cefa99e714d8619a32827ad1", + "zh:2a0cfc5600730093705071707e4a4e4e953e7d9091859e0f66b46daa1060dd5d", + "zh:2ff53eac1af43ab9a2248a0e53c963d46e19cf04bc4c3f323591cfcebb218252", + "zh:4ebc3dee700f60af9da29970052fd02fa947813162b224716862dc9d7f1f7542", + "zh:5fe6dab84ceeaa8eb3f1567c5f05578333370c472240ca5c5bfc25e92d4d5586", + "zh:66bbec16367bbf440045502c9779b11f4ac5b022c8d8d17afe12d431950838b5", + "zh:7641e5c2e4b529e869cde29ab5b1de2fd1091489eb745b19ac2709bd7f4dfd84", + "zh:855bfba0756d17ce07595ff57d7cf664443d1495127cb88fb063362734b8b22a", + "zh:aaec10f237921d60c581d1b7a66f0a8a8019d9802dc04af11b5b981f6682e01d", + "zh:e460835a38ffa1e74f6929904bfd14ef473d217fd537b7ce834abe5ce5e2ce07", + "zh:ecc4295215db0e4aea3c9329611c31e09a853e1ae207d56742403bd4f5516703", + "zh:ee6d9fae63a612072e00402894e14826af7a3351c235b9c5b423b7629a77ca29", + "zh:f2b5c8db74aa7ebcf7cd423672358437d42401675069ef67b01ff910054e49d5", + "zh:f5aff74d3eb96d4592c7bca5cd3ea89b469e84efbf382944bd0f844a57059c09", + ] +} + +provider "registry.opentofu.org/hashicorp/tls" { + version = "4.3.0" + constraints = "~> 4.0" + hashes = [ + "h1:+15bx0zcnqLz534Cbb8IC+FHMqfG55ELn+PxrQDA4gc=", + "h1:CzxZKtqwgNLbLx29KpIG8H3jOQFRkXCerkDRafS6PCg=", + "h1:EF8ln/osHphljZ9LNlqUICducYCjUq+PSaC2wBZM6cA=", + "h1:GizReb5vbh71HnhHlGphHhVFj3ghwAaC2MKqb2d8Ye8=", + "h1:O5oOot0y1MLb/j2gXVAOd20bPzM0daZyqgCk4kHbihg=", + "h1:SGBiqFFxGryTOiaNNWlC7CCba1hjSsSwcJeg6rOLJrc=", + "h1:UAV4fX41sizZ4U+FavGu3Fkgm4g7k8JD7BqBuhjMZ7o=", + "h1:ZxKvDInYHzss9rv75M778pInFm08ME6hY31XMyFP4IA=", + "h1:fpHTjAZkKqg+bRAiHmNzsMtYOPleAuK0hpdJxTLPtJE=", + "h1:hC3YGicct3gfUaMeY/Ci1MbS0ieDr6POkU8sudjkwKE=", + "h1:jJrKC+VUBdAAfBlcB06mNmlGskdd+MGoQI34hsMLItY=", + "h1:mmFJoeY9KBishP/zH8vvtpDekcqiYucgUGUnErhECAY=", + "h1:uMVBRi+9fgKgigHapewZE/BPiu/GfPvXlor/N7glFTk=", + "h1:xCRJdZECen3k+Qct+nUrUkUG5wpbeSiGHQGIJpgdkxg=", + "h1:xX++0TgL6lEWjSs33i3gI07CL1uIcOvUSweNkhvXK78=", + "zh:07bb8c6e64124dada7dff57a38a46f2f323b3fd77920404c0c550293d1cf6188", + "zh:0b3bfda2df39c52f1c5452d05cf3107bedd5d20ab6977c90ede540c695fb6c3e", + "zh:110a055289f0400a63ac172bedb0e671d059b7a5ba22d4a3f5f246ccac0ad676", + "zh:15e532d8c711377499dece832e60170a8bef39830125b8154f4bda81d9721d29", + "zh:22ca65d96e9fc1be5605372d855c9e1eba2d86d510f7ac8593968f5649435e47", + "zh:36df38dfd03e8c1298c5704fd85e28b69a3927ed0b339f9628d0b56dac99c6b5", + "zh:429e2bfcb81656e1fe90b7b284767d1453c1a4100b16d27e4b29c34aa12f0ce1", + "zh:5b6679953065f0279bf018426c6fb06dd93a851a7a9369f2e3a1fec5bc417e83", + "zh:6a72c88d5aa945ddb32041350755377c96681563136decfe7e05c7cdea7988f1", + "zh:6f05757c50da9f8354a735b5756bd63a71126fcd142129525b90c56bfd081d61", + "zh:751703b7a4d40c3a111c4ed0d5da3ec91c14f880faf6f010a5000a2eb5366011", + "zh:87a5279e61b8198798a2fe86cfe3b74e5340bb486f4e148bb5b4d46f860cf1db", + "zh:942af95e9fd73327a7e9ab0803c4d701b782ddacd78c9b7ce9c91e38b3051522", + "zh:a457d0efea3c404178a182d240ba21cdeb0c620ffabeeb9a8977b024a85e1360", + "zh:d5eac8f4f0ae1ff41cbcc1008e6a74a8491dc27f4c6e5a0c32c5c4b6ef2e4087", + ] +} From df98874ec062b0a2dd09eeeb2bdcbdb1d368b6fd Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Sun, 12 Jul 2026 15:00:25 -0400 Subject: [PATCH 6/8] fix: add config.Init() to runcmd and ssm PersistentPreRunE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same pre-existing bug as bastion — child commands override the root PersistentPreRunE without calling config.Init(), so the config file is never loaded. Co-Authored-By: Claude --- cli/cmd/runcmd.go | 3 +++ cli/cmd/ssm.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/cli/cmd/runcmd.go b/cli/cmd/runcmd.go index 84ded1d6..018dee29 100644 --- a/cli/cmd/runcmd.go +++ b/cli/cmd/runcmd.go @@ -25,6 +25,9 @@ no inbound ports). Caveats vs. AWS SSM: each invocation is one-shot (~5-15s latency), output is capped at 4096 bytes per stream, and there are no persistent sessions to list or clean up.`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if err := config.Init(); err != nil { + return err + } cfg, err := config.Get() if err != nil { return err diff --git a/cli/cmd/ssm.go b/cli/cmd/ssm.go index befb8f2f..a922e7c3 100644 --- a/cli/cmd/ssm.go +++ b/cli/cmd/ssm.go @@ -19,6 +19,9 @@ var ssmCmd = &cobra.Command{ Long: `SSM commands are AWS-specific. For Azure use 'dreadgoad runcmd' (Azure Run Command). For other providers see their respective verbs.`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if err := config.Init(); err != nil { + return err + } cfg, err := config.Get() if err != nil { return err From 11a5eb8408c5cf0aee312360f9013f5cc40af077 Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Sun, 12 Jul 2026 15:05:44 -0400 Subject: [PATCH 7/8] fix: use terraform registry in lock file and guard authType on key Replace tofu-generated lock file (registry.opentofu.org) with terraform registry hashes to match the other modules. Also only set authType to ssh-key when the ephemeral key file actually exists on disk. Co-Authored-By: Claude --- cli/cmd/bastion.go | 2 +- .../terraform-azure-kali/.terraform.lock.hcl | 171 ++++++++---------- 2 files changed, 78 insertions(+), 95 deletions(-) diff --git a/cli/cmd/bastion.go b/cli/cmd/bastion.go index 13f90cf5..34903108 100644 --- a/cli/cmd/bastion.go +++ b/cli/cmd/bastion.go @@ -204,7 +204,7 @@ func runBastionSSH(cmd *cobra.Command, args []string) error { // Auto-pick the ephemeral key for known VM roles. A failed live lookup // is non-fatal — we just fall back to the user-supplied flag values. if defaults := resolveRoleDefaults(client, ctx, cfg.Env, args[0]); defaults != nil { - if !cmd.Flags().Changed("auth-type") { + if !cmd.Flags().Changed("auth-type") && defaults.sshKey != "" { authType = defaults.authType } if !cmd.Flags().Changed("ssh-key") && authType == "ssh-key" && defaults.sshKey != "" { diff --git a/modules/terraform-azure-kali/.terraform.lock.hcl b/modules/terraform-azure-kali/.terraform.lock.hcl index ed3721d5..904cb566 100644 --- a/modules/terraform-azure-kali/.terraform.lock.hcl +++ b/modules/terraform-azure-kali/.terraform.lock.hcl @@ -1,113 +1,96 @@ -# This file is maintained automatically by "tofu init". +# This file is maintained automatically by "terraform init". # Manual edits may be lost in future updates. -provider "registry.opentofu.org/hashicorp/azurerm" { +provider "registry.terraform.io/hashicorp/azurerm" { version = "4.80.0" constraints = "~> 4.0" hashes = [ - "h1:/3VCHqW2jY/9fum11q23cxIeVLBi3nm5cufGvFN7pP4=", - "h1:3ugN9+6K4hJXpVvISHpyEfit+nj6KDH9WmYU01uIfsw=", - "h1:4WDTvTdpSZLSYuwSNfDS7qcCchUUYHec027bF41gb7s=", - "h1:9cgluGNXUB/ILYC2xHoop97QqqVkLkEzUe5yoQrllP4=", - "h1:BkDHI0+fPlHKM9PIOKnOwGUxgYOV65O2P4cWebI3aB0=", - "h1:C9yMt4aisAy2Tc4RYqezUHaho8yUB3Cyqg1Beach7bY=", - "h1:I8uN35ybRHD0rmvjE+lh+bCFLM6vM1p8QLm2RIsn20Q=", - "h1:KbYTNYdayAxMb8aEp767242j0/JU3/ri2XH05eURCFY=", - "h1:MNvJ+4ccdPV0bIKLimKkmvUAuuCdx2a1MoR72vjABsI=", - "h1:WZuPh5xdWlnHlKH8yWrG+7ZkL2nvooKGscBJAKA5fv8=", - "h1:bpmNZO9WMHOOknKjZQEV72Rwjjfiks049RCP1gKTaoI=", - "h1:eAy7agtpfkC+6eHb7NmtTNXcDd67qvtfbRFvLCD0n6w=", - "h1:p0/RKU7/erfKriGOjgQBAQHjJkX2oWfvAri+euYPV9A=", - "h1:p6UGxTkUbB2OjomDXN/6PGZAxfxKoK1BhqOLZaBl3YA=", - "h1:suntvZgX/12BEoFZzn3WVAWzPzm/41urdh1zl1DkfcE=", - "zh:0737e143b21aed8a9dd8230c913cb653a127a52f335dc52f1d212593503117dc", - "zh:0c0262a7cda79d3e850545a74fe3151dd00e2b0a9923cfdb101fe05c1271404f", - "zh:211586e6317b9c432d1d1dfa2ddb72f9a25704088b5c77ce7b78eede94eb3209", - "zh:2eb97f4fdc5dd2550d300403ba5c8a84a40c776a957126cdda4c7a3fca11bdda", - "zh:3218e942b8417775d45ba4d23dc777649c0a5bbe5d2848a53b90c1ba494660cc", - "zh:38a747101fa3a95ebd0390b8382d02ed016ea0cfba61b9b9a8f0b2ff2b8ae31d", - "zh:4151feabbb9e15b8dc5d7e4b2258ed4a4d2ff0f467fdfe7c805075235da3b92a", - "zh:5a589ac3bd83b0db1db60ff1001baea94c1d35be8fe2c4526ba8224d06769dc9", - "zh:753ef483a3af84d23750556aa8ba87cdb98ea57bf148c4e7a65dc6be7c7f3f5c", - "zh:a8e9273362cc670ec44769b71e32e128fcb25cfeb180903673bcb19a020aa73d", - "zh:bbd6f1099e386c987d0d0f16428dcde5f6c50e94d6a65c98648441f357d608d1", - "zh:d43989efee459f4dc43267fbc98f3ee8d8d6066c829db1627d305d633019d538", - "zh:d9036449c588ad700df2f254a69d828098b44d55ffb8ec52c31b27b0ab21d871", - "zh:e9e116793807c0af42de1d16ebb1b8c3bf0252f1eb9645f7662102fa80d8f8b0", - "zh:ec0e7e9a066f2a9f806afa2aa96fa21e2995ee85cfb40dbf96179dbe8e19f790", + "h1:/HSKYi2kpEnIs2AVYmWSOCq98T8twsV+OIopjX8f8rQ=", + "h1:9w0W319/O9peNiJZIwBaWPYyvOrZ8RVr0le6ixXT66Q=", + "h1:Cv5TQOTEGTV806bN4cvyyuToG6V4LV+cLAlWNU9FmXU=", + "h1:IGLCHEb0I3CGzLdrzzD7E3aUDrUDQt3dT6FYL1qQtSQ=", + "h1:R8n0T2WWUxa65nw64W/Q+M2X9My84/lebmKwAzixlu8=", + "h1:VKFPEqVn6/84AoY3BkoF+dBsBWNp4xRYyENlnA6j6o4=", + "h1:W7kIVPIabNADsBiTKTW/vlQnG//YukDMhCh5njp02hU=", + "h1:d8YcvRsfL2rg8S3rkdl+rYd0TY8WGLzVqIEtv9vacAA=", + "h1:eTDAe+AoQmlj7LENZPWvlNIz2GiEPluu9b0nduAN1rk=", + "h1:kuLwWwnQ0b9o0YE6/k/SeMSBv0F/O1nebnUfpKujzds=", + "h1:sxndDh73F1YkGhSnUQ2IfvixItMewzCKaX5H9TPfTZs=", + "zh:1287b44676ced8f2d8131b1746a027f05edba2e5aa7e3ecdbaa6887aaad68431", + "zh:1ae7263cafd4f9ffff6a595190ee940af929bda026bddd7db2cd733596878597", + "zh:3ec5bdbce4ce98db2f850d98c75024d7267df9809ad8ef425ec21e727858d150", + "zh:4a33b42598fe7337d8e78b0ab57daff65a0689e0896c1232d380511e0a011dc7", + "zh:69348cf4aaa49869ccb66aedc1ff99aac59fccf7c1971b3829d5f372291d847a", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7cfbfca11cb85713fa0666f788b5c5778f38ff88b1956c61a638b1fb0a02773c", + "zh:7e889f4d9c5907e037f3ea486dd517e5481342625635b10a4cc7338a5571cdc0", + "zh:85bc8cf551491ffcdf841110922b69d040090f379924896b2ccbc337cc7cb41e", + "zh:99585400adafdc8aedd37cb1e715e986ce39612ba18bab34fd8749aae37593d7", + "zh:a1d4ccf5e1afb4e440119b3ff2e41c3192979ea9b1735cc9d91a7fb8684339e0", + "zh:ea3be732d424c36634dd105425879ea7610224cada0c59ed6172ce67b610289d", ] } -provider "registry.opentofu.org/hashicorp/local" { +provider "registry.terraform.io/hashicorp/local" { version = "2.9.0" constraints = "~> 2.5" hashes = [ - "h1:1dtKYW/5a1qob3yneL6WzOlnSGfYtJ6a2XeejCk9yb4=", - "h1:5NseXq5wU8O20ersTtV4ocrLYFFtgFr7n0pRLO1W2Rw=", - "h1:5d22ZPPK4iiygPbwRz/PJF5Es/0axVpMlPRpCR0Padw=", - "h1:AnwyolirmIlBMjH6+tV8bKkvT+5axJNYxi2y2IguiX4=", - "h1:PBp+HeseY021Fw3sLznCG27idgwPoff4cBuNmKgPL2w=", - "h1:VDxIhe4GbzdOCdmt7mQaqdwERQW6GSI7Roonts42Gr0=", - "h1:ZO6eWWnf8LjjV1q/JNeL9WLtZ6fwIttOnyN5LjCNSEo=", - "h1:dPIAf8oUAz+vW2E0iZunMvpuPddRZIztRsPSY1u+VnY=", - "h1:fwTDVG9AhFVKQZIb1EXkHv4FqzsZNlLWgkyPGDmZZEE=", - "h1:kDc465XPC7/6XFCjrMC4mTqhA9ef0FHKuJ3ZgfGNfeg=", - "h1:kGbjxrI2P8MHeyVtE1U3Q1TbyF71ExnHxtkrE+Aj6UU=", - "h1:kcoK6Afbsj54u9zaEqpecWAFKytqjBijtguCNwV3d4M=", - "h1:rxomJjDwOo+YZ+WIPc25FqEgsz9orh/2MCyUcZmFjvw=", - "h1:t0CMn/Rkwquw8l2yQ+O4ApzbMZfY2UazbsDnZygzACA=", - "h1:tJwgm2BS4xCGlElCDQEFXQoefY9Y4t0JdSKTtsPBbBo=", - "zh:13ef7ecd1e397ec5b20ea588508dd3e3b8d6c50d809ae76b079abf9dd8d02e4b", - "zh:2190c9325980076489ce02b0f5dd2c0b91fc8711cefa99e714d8619a32827ad1", - "zh:2a0cfc5600730093705071707e4a4e4e953e7d9091859e0f66b46daa1060dd5d", - "zh:2ff53eac1af43ab9a2248a0e53c963d46e19cf04bc4c3f323591cfcebb218252", - "zh:4ebc3dee700f60af9da29970052fd02fa947813162b224716862dc9d7f1f7542", - "zh:5fe6dab84ceeaa8eb3f1567c5f05578333370c472240ca5c5bfc25e92d4d5586", - "zh:66bbec16367bbf440045502c9779b11f4ac5b022c8d8d17afe12d431950838b5", - "zh:7641e5c2e4b529e869cde29ab5b1de2fd1091489eb745b19ac2709bd7f4dfd84", - "zh:855bfba0756d17ce07595ff57d7cf664443d1495127cb88fb063362734b8b22a", - "zh:aaec10f237921d60c581d1b7a66f0a8a8019d9802dc04af11b5b981f6682e01d", - "zh:e460835a38ffa1e74f6929904bfd14ef473d217fd537b7ce834abe5ce5e2ce07", - "zh:ecc4295215db0e4aea3c9329611c31e09a853e1ae207d56742403bd4f5516703", - "zh:ee6d9fae63a612072e00402894e14826af7a3351c235b9c5b423b7629a77ca29", - "zh:f2b5c8db74aa7ebcf7cd423672358437d42401675069ef67b01ff910054e49d5", - "zh:f5aff74d3eb96d4592c7bca5cd3ea89b469e84efbf382944bd0f844a57059c09", + "h1:0W7SDCcshaGz7gFLqRh+xSybU7CsiffGtYxLyGkd5v8=", + "h1:19szYap0j+IaX3fdQkVlT/xXEAkyXZGoabKsK7RdLWU=", + "h1:9rBZCMNpxKwMlRbWH2QpwD3kqUCAejdOZQ/aiiDObXQ=", + "h1:F8pWDG0pUb3qa0pWXEgjxXZuIVtRrcwnK4OkMKb/Gqc=", + "h1:JFNiGJRidyYACpbXswqoX4cjdVkvNdYA/e4YhvA/M/w=", + "h1:M4Ij0M7djWAH1hWcXCfUeyH8BioaiK+UzrM8elq6jTQ=", + "h1:SjeVipWRhcQvyypSYSEtn95sSbiqbV0XH8BnkGoIAtU=", + "h1:Vxi5xD5rBImMA83gSYnJmLCodG0kUqGQHVYBxpgYj+E=", + "h1:m24fjcInWvTVZ1XSo2MaNuKPe+X/gfG8SIi09rA7a7M=", + "h1:oF4vw1rikPMqQtMMBoUjc/pERp50DlWwO+C2I6/sNI8=", + "h1:px3Hpv/tL288wzu5knHywTTBcrydLnnGEiF/NIQBaRs=", + "h1:zi/RbdLCYxU9upG9loL7b+M694GkW6kACIYJMkVdQVs=", + "zh:0baa4566cf77f1ff52f4293d1c8536202dd23edc197c3196413a28343c3ac3a0", + "zh:16b5559c3c07088ddad11a9bb9e9c0799999363c2958e9a5be2bcbbf2cd9ca64", + "zh:197c79015a10d1cce904a8ea722cbc750c42aeae2da53f44a6a0751d9fd1aa90", + "zh:29d0b03e5343a80677ebfeb2e2c31cbe4b1f65e736e53417454a4277fec2544c", + "zh:4896bfa6cf1d2fd562b47ef2e87f47862ae92a04f8ad5d764380f0c6653473b8", + "zh:531f8529cbca49f681883e57761a05a8398afaef6d1ab0d205d26bf12f4428e8", + "zh:6aaf5011d83161c86d2bfb80c0923ec934e578288758da2f37acb7aec129004b", + "zh:7430275253d3d3c40aa6179e0ec0d63212874dbbc06c5a51b9d07ec590f9756c", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:be17dc611e95e26cdf6cad79dfccf1064f0e32032a2efeb939a9bbe7fb1cbfe9", + "zh:f0e3b0aa644202e1d79d2000dca91f6019425da71e9800fa23f27e51c034f195", + "zh:f62bae4519e4ead49182ddc8afe8cf61e2a4c3ba3973b0fbba967736a2696aa3", + "zh:fcafa360a5b0b96244f26f4e3a6d642b716a376557142c2442ff2fb12d11da18", ] } -provider "registry.opentofu.org/hashicorp/tls" { +provider "registry.terraform.io/hashicorp/tls" { version = "4.3.0" constraints = "~> 4.0" hashes = [ - "h1:+15bx0zcnqLz534Cbb8IC+FHMqfG55ELn+PxrQDA4gc=", - "h1:CzxZKtqwgNLbLx29KpIG8H3jOQFRkXCerkDRafS6PCg=", - "h1:EF8ln/osHphljZ9LNlqUICducYCjUq+PSaC2wBZM6cA=", - "h1:GizReb5vbh71HnhHlGphHhVFj3ghwAaC2MKqb2d8Ye8=", - "h1:O5oOot0y1MLb/j2gXVAOd20bPzM0daZyqgCk4kHbihg=", - "h1:SGBiqFFxGryTOiaNNWlC7CCba1hjSsSwcJeg6rOLJrc=", - "h1:UAV4fX41sizZ4U+FavGu3Fkgm4g7k8JD7BqBuhjMZ7o=", - "h1:ZxKvDInYHzss9rv75M778pInFm08ME6hY31XMyFP4IA=", - "h1:fpHTjAZkKqg+bRAiHmNzsMtYOPleAuK0hpdJxTLPtJE=", - "h1:hC3YGicct3gfUaMeY/Ci1MbS0ieDr6POkU8sudjkwKE=", - "h1:jJrKC+VUBdAAfBlcB06mNmlGskdd+MGoQI34hsMLItY=", - "h1:mmFJoeY9KBishP/zH8vvtpDekcqiYucgUGUnErhECAY=", - "h1:uMVBRi+9fgKgigHapewZE/BPiu/GfPvXlor/N7glFTk=", - "h1:xCRJdZECen3k+Qct+nUrUkUG5wpbeSiGHQGIJpgdkxg=", - "h1:xX++0TgL6lEWjSs33i3gI07CL1uIcOvUSweNkhvXK78=", - "zh:07bb8c6e64124dada7dff57a38a46f2f323b3fd77920404c0c550293d1cf6188", - "zh:0b3bfda2df39c52f1c5452d05cf3107bedd5d20ab6977c90ede540c695fb6c3e", - "zh:110a055289f0400a63ac172bedb0e671d059b7a5ba22d4a3f5f246ccac0ad676", - "zh:15e532d8c711377499dece832e60170a8bef39830125b8154f4bda81d9721d29", - "zh:22ca65d96e9fc1be5605372d855c9e1eba2d86d510f7ac8593968f5649435e47", - "zh:36df38dfd03e8c1298c5704fd85e28b69a3927ed0b339f9628d0b56dac99c6b5", - "zh:429e2bfcb81656e1fe90b7b284767d1453c1a4100b16d27e4b29c34aa12f0ce1", - "zh:5b6679953065f0279bf018426c6fb06dd93a851a7a9369f2e3a1fec5bc417e83", - "zh:6a72c88d5aa945ddb32041350755377c96681563136decfe7e05c7cdea7988f1", - "zh:6f05757c50da9f8354a735b5756bd63a71126fcd142129525b90c56bfd081d61", - "zh:751703b7a4d40c3a111c4ed0d5da3ec91c14f880faf6f010a5000a2eb5366011", - "zh:87a5279e61b8198798a2fe86cfe3b74e5340bb486f4e148bb5b4d46f860cf1db", - "zh:942af95e9fd73327a7e9ab0803c4d701b782ddacd78c9b7ce9c91e38b3051522", - "zh:a457d0efea3c404178a182d240ba21cdeb0c620ffabeeb9a8977b024a85e1360", - "zh:d5eac8f4f0ae1ff41cbcc1008e6a74a8491dc27f4c6e5a0c32c5c4b6ef2e4087", + "h1:5bCU/c+2HUh7GhclzNSH6gAuoCS4inW3obEtRAwu6WQ=", + "h1:5xblgWaLCk0/S5sSDMEBpUO31Kv9eyvo2e0qvKDvFKg=", + "h1:7QWrBlzkkFAFyDl9UsfC0tdfNFquFx03miHwZcta33Q=", + "h1:9O6c6A3Pa8DaW0UlTqjVy9wamwN8+2xG+wfukhnVouk=", + "h1:Nx1AbmRplFV9vyOBwlBth+MyD5fth6Snv8lIFOsWvu0=", + "h1:OeJhJw1X6OeRqt8n4xMxJ8SQGTudxKT0ehqC8HxWpTE=", + "h1:QO1mfwRWprT41JDGRUfpIYyFs8h63yeOsB8RfKOjNYU=", + "h1:QjGpJjmvOB973D0BM9rPv/h9ln+wM64IGY/tfuOginw=", + "h1:Ubkf73KvM44o9Gh5+n9v+EskNjqGGl4wT29mQjmyUQE=", + "h1:eDtm+6iX4ydUbWFwQEyHzR8ukb+o5784XjotMq0C9yQ=", + "h1:iVr2IDV+RRoxAra3YM6w4jVsF052ougqMjbfcTtdJsw=", + "h1:j/BqLS2N2AScZyotd9nZpHdieJ7e5S8y+A+ZfIu8kL8=", + "zh:0ab58d6f8991d436c7d2dbd89ed814709b949b07ac5a54ee53b0aec1fa772a8b", + "zh:60b347abcb56f45d97c56f14d895069cd15a83993f199777f571b79fea3642ee", + "zh:6889be32640349230de3f23856e6f04e0e9ced4a84a27d3f552fa54684448218", + "zh:73f8e1ecf7135033165fb14b7e8bf4d656f3ce13065ec35762ea0481975328c7", + "zh:94ce25ee253eca0b42cae9c856b36bca8103b6453012d1b279c3623c805f2d42", + "zh:96bc6de9fd67bc446fd11257872e1ffb1029a996ed1d65a3f6b43f6d408ad9ab", + "zh:97c609a310a51bfd504d704e036d72064a84bf0bdb36cc08cd4cc66098212b41", + "zh:a12c16e94533c5bd123f75032576b9dc91dd5d5ccd5f7cf331d0f2e1adc55cf8", + "zh:c4f014f876adf7af57188795050bda5b0029d8c7d7773031102b6c36dcf1fc21", + "zh:d9b0a21583aaa3df3a95394fb949a3c515ff71c2ff5a1fc4a73d364aa90bfca5", + "zh:da510d22f0c6d71ad19a76406f106b782448f512375787ecfabb338ed1e311a7", + "zh:f0e9447a9ce3a24cdaa113089e65663c836d8b9bfdb915a1c0284e0112cab5c0", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } From 96eed1e7eb7171ff82fd51d85f4daaf35fbc6fb6 Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Sun, 12 Jul 2026 15:32:47 -0400 Subject: [PATCH 8/8] fix: gate all role defaults on key existence and case-insensitive terms check Only apply role-based username/authType when the ephemeral key file exists, preventing password-auth fallback on key-only VMs. Also use EqualFold for the marketplace terms check since az CLI may return "True" instead of "true". Co-Authored-By: Claude --- cli/cmd/bastion.go | 6 +++--- cli/internal/doctor/checks.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/cmd/bastion.go b/cli/cmd/bastion.go index 34903108..0f5fd76b 100644 --- a/cli/cmd/bastion.go +++ b/cli/cmd/bastion.go @@ -203,11 +203,11 @@ func runBastionSSH(cmd *cobra.Command, args []string) error { // Auto-pick the ephemeral key for known VM roles. A failed live lookup // is non-fatal — we just fall back to the user-supplied flag values. - if defaults := resolveRoleDefaults(client, ctx, cfg.Env, args[0]); defaults != nil { - if !cmd.Flags().Changed("auth-type") && defaults.sshKey != "" { + if defaults := resolveRoleDefaults(client, ctx, cfg.Env, args[0]); defaults != nil && defaults.sshKey != "" { + if !cmd.Flags().Changed("auth-type") { authType = defaults.authType } - if !cmd.Flags().Changed("ssh-key") && authType == "ssh-key" && defaults.sshKey != "" { + if !cmd.Flags().Changed("ssh-key") && authType == "ssh-key" { sshKey = defaults.sshKey } if !cmd.Flags().Changed("user") { diff --git a/cli/internal/doctor/checks.go b/cli/internal/doctor/checks.go index 3fe2f077..be2d56c8 100644 --- a/cli/internal/doctor/checks.go +++ b/cli/internal/doctor/checks.go @@ -425,7 +425,7 @@ func checkKaliMarketplaceTerms() CheckResult { "Required if using --with-kali", } } - if strings.TrimSpace(string(out)) == "true" { + if strings.EqualFold(strings.TrimSpace(string(out)), "true") { return CheckResult{ Name: "Kali marketplace terms", Status: "pass",