{"record":{"id":"b36a1d88c11be1dd","repo":"kubernetes/kops","slug":"bastion-is-not-set-but-usebastion-is-true","errorCode":null,"errorMessage":"bastion is not set, but useBastion is true","messagePattern":"bastion is not set, but useBastion is true","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/dump/dumper.go","lineNumber":676,"sourceCode":"type sshClientFactoryImplementation struct {\n\tbastion   string\n\tsshConfig *ssh.ClientConfig\n\tkeyRing   agent.Agent\n}\n\nvar _ sshClientFactory = &sshClientFactoryImplementation{}\n\n// HasBastion implements sshClientFactory::HasBastion\nfunc (f *sshClientFactoryImplementation) HasBastion() bool {\n\treturn f.bastion != \"\"\n}\n\n// Dial implements sshClientFactory::Dial\nfunc (f *sshClientFactoryImplementation) Dial(ctx context.Context, host string, useBastion bool) (sshClient, error) {\n\taddr := host\n\tif useBastion {\n\t\tif f.bastion == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"bastion is not set, but useBastion is true\")\n\t\t}\n\t\taddr = f.bastion\n\t}\n\n\tif addr == \"\" {\n\t\treturn nil, fmt.Errorf(\"host is empty\")\n\t}\n\taddr = net.JoinHostPort(addr, \"22\")\n\td := net.Dialer{\n\t\tTimeout: 5 * time.Second,\n\t}\n\tconn, err := d.DialContext(ctx, \"tcp\", addr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error dialing tcp %s: %w\", addr, err)\n\t}\n\n\t// We have a TCP connection; we will force-close it to support context cancellation\n","sourceCodeStart":658,"sourceCodeEnd":694,"githubUrl":"https://github.com/kubernetes/kops/blob/4c8573c808a73d578c5eadc86d410646ea0b0d73/pkg/dump/dumper.go#L658-L694","documentation":"sshClientFactoryImplementation.Dial (pkg/dump/dumper.go:672) is asked to open an SSH connection routed through a bastion host, but the factory was constructed with an empty bastion field. The library refuses to silently fall back to a direct connection because the caller explicitly requested bastion routing (useBastion=true), which would change the security/network topology. It fails fast with this error instead.","triggerScenarios":"Calling Dial(ctx, host, true) on an sshClientFactoryImplementation whose bastion field is \"\". In practice this happens when a kOps cluster dump is run against a topology that has no bastion (or a bastion instance that was deleted) while the dumper still computes useBastion=true for nodes behind private subnets.","commonSituations":"Cluster built with topology without a bastion but instance groups marked as private; bastion deleted or never created while cluster spec still references it; code that sets useBastion based on subnet privacy rather than HasBastion(); stale cached cluster state after a topology change.","solutions":["Check f.HasBastion() before passing useBastion=true to Dial, and fall back to useBastion=false if no bastion exists","Verify the cluster has a bastion instance (kops get cluster -oyaml: topology.bastion / bastion instance group) and create it if missing (kops update cluster)","Ensure the sshClientFactory is built with the bastion public address populated (see where sshClientFactoryImplementation{} is constructed in pkg/dump)"],"exampleFix":"// before\nclient, err := factory.Dial(ctx, nodeIP, isPrivateSubnet)\n\n// after\nuseBastion := isPrivateSubnet && factory.HasBastion()\nclient, err := factory.Dial(ctx, nodeIP, useBastion)","handlingStrategy":"validation","validationCode":"if useBastion && !factory.HasBastion() {\n\treturn fmt.Errorf(\"cannot dial %s: bastion routing requested but no bastion is configured\", host)\n}\nclient, err := factory.Dial(ctx, host, useBastion)","typeGuard":"func canUseBastion(f *dump.SSHClientFactory) bool { return f.HasBastion() }","tryCatchPattern":"client, err := factory.Dial(ctx, host, useBastion)\nif err != nil {\n\tif strings.Contains(err.Error(), \"bastion is not set\") {\n\t\tclient, err = factory.Dial(ctx, host, false) // fallback to direct\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n}","preventionTips":["Always gate useBastion on factory.HasBastion()","Keep cluster topology (bastion presence) in sync with the dumper configuration","Add a unit test covering Dial with useBastion=true on a bastion-less factory"],"tags":["ssh","bastion","configuration"],"backgroundTag":"bastion-not-configured","analyzedSha":"4c8573c808a73d578c5eadc86d410646ea0b0d73","analyzedAt":"2026-09-05T04:13:19.212Z","contentChangedAt":"2026-09-05T04:13:19.212Z","schemaVersion":2},"datasetVersion":"2026-09-12T12:17:11.808Z"}