Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions cmd/sandbox/editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,13 @@ func hostLine(alias, name string) string {
}

// renderSSHBlock builds the ~/.ssh/config stanza for the sandbox.
//
// HostKeyAlias pins each sandbox's known_hosts entry to its own id. Without it
// every tunnel-mode box is keyed on the shared `127.0.0.1:22`, so the second
// sandbox trips a CHANGED HOST KEY warning and clients that treat that as an
// attack (Orca's SSH relay, for one) refuse to connect. VPN mode keys on the
// overlay IP, which is also recycled between sandboxes, so it needs the same
// pin.
func renderSSHBlock(alias, mode, sandboxID, sbIP, gwHost string, gwPort int, user, identity, name string) (string, error) {
begin := fmt.Sprintf(sshConfigBlockBegin, alias)
end := fmt.Sprintf(sshConfigBlockEnd, alias)
Expand All @@ -483,13 +490,14 @@ func renderSSHBlock(alias, mode, sandboxID, sbIP, gwHost string, gwPort int, use
return fmt.Sprintf(`%s
Host %s
HostName %s
HostKeyAlias %s
Port 22
User %s
IdentityFile %s
StrictHostKeyChecking accept-new
UserKnownHostsFile ~/.ssh/known_hosts_createos
%s
`, begin, host, sbIP, user, identity, end), nil
`, begin, host, sbIP, sandboxID, user, identity, end), nil
case "tunnel":
// The inner `ssh -W` for the gateway needs its own
// StrictHostKeyChecking + UserKnownHostsFile — it doesn't inherit
Expand All @@ -498,14 +506,15 @@ Host %s
return fmt.Sprintf(`%s
Host %s
HostName 127.0.0.1
HostKeyAlias %s
Port 22
User %s
IdentityFile %s
ProxyCommand ssh -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=~/.ssh/known_hosts_createos -W %%h:%%p %s@%s -p %d -i %s
StrictHostKeyChecking accept-new
UserKnownHostsFile ~/.ssh/known_hosts_createos
%s
`, begin, host, user, identity, sandboxID, gwHost, gwPort, identity, end), nil
`, begin, host, sandboxID, user, identity, sandboxID, gwHost, gwPort, identity, end), nil
default:
return "", fmt.Errorf("unknown mode %q", mode)
}
Expand Down
58 changes: 58 additions & 0 deletions cmd/sandbox/editor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package sandbox

import (
"strings"
"testing"
)

// Both transports must pin known_hosts to the sandbox id. Tunnel mode shares
// 127.0.0.1:22 across every box and VPN mode recycles overlay IPs, so without
// the pin the second sandbox reads as a changed host key.
func TestRenderSSHBlockPinsHostKeyAliasToSandboxID(t *testing.T) {
for _, mode := range []string{"tunnel", "vpn"} {
t.Run(mode, func(t *testing.T) {
block, err := renderSSHBlock(
"sb-aaa", mode, "sb-aaa", "10.0.0.7",
"gateway.sb.createos.sh", 2222, "root", "/keys/sb-aaa", "my-box",
)
if err != nil {
t.Fatalf("renderSSHBlock: %v", err)
}
if !strings.Contains(block, "HostKeyAlias sb-aaa") {
t.Errorf("block is missing the HostKeyAlias pin:\n%s", block)
}
})
}
}

func TestRenderSSHBlockGivesEachSandboxADistinctHostKeyIdentity(t *testing.T) {
render := func(id string) string {
block, err := renderSSHBlock(
id, "tunnel", id, "10.0.0.7",
"gateway.sb.createos.sh", 2222, "root", "/keys/"+id, "",
)
if err != nil {
t.Fatalf("renderSSHBlock(%s): %v", id, err)
}
return block
}
if aliasLine(render("sb-aaa")) == aliasLine(render("sb-bbb")) {
t.Error("two sandboxes share one host-key identity; the second will trip a changed-host-key error")
}
}

func TestRenderSSHBlockRejectsUnknownMode(t *testing.T) {
if _, err := renderSSHBlock("sb-aaa", "carrier-pigeon", "sb-aaa", "10.0.0.7",
"gateway.sb.createos.sh", 2222, "root", "/keys/sb-aaa", ""); err == nil {
t.Error("expected an error for an unknown mode")
}
}

func aliasLine(block string) string {
for _, line := range strings.Split(block, "\n") {
if trimmed := strings.TrimSpace(line); strings.HasPrefix(trimmed, "HostKeyAlias") {
return trimmed
}
}
return ""
}