package keymgr import ( "fmt" "strings" "ssh-access-tui/internal/model" ) // RenderClientConfig produces an OpenSSH client config (works identically on // Windows' built-in OpenSSH client, macOS, and Linux). It defines exactly // one connection, "jump", carrying one LocalForward per allowed service — // the user opens it once (see INSTRUCTIONS.txt) and every service becomes // reachable at once on the listed local ports, for as long as that one // connection stays open. // // This file is convenience only, not a security boundary: nothing stops a // user from editing it or invoking ssh by hand. The actual restriction to // these destinations is enforced server-side via authorized_keys // permitopen= (see internal/sysadm/authorizedkeys.go) independent of what // this file says. func RenderClientConfig(jumpHost string, jumpPort int, username, keyFileName string, forwards []AllocatedForward) string { var b strings.Builder fmt.Fprintf(&b, "# ssh-access-tui client config for %s\n", username) fmt.Fprintf(&b, "# Generated %s. Regenerated whenever your allowed services change.\n\n", genTimestamp()) fmt.Fprintf(&b, "Host jump\n") fmt.Fprintf(&b, " HostName %s\n", jumpHost) fmt.Fprintf(&b, " Port %d\n", jumpPort) fmt.Fprintf(&b, " User %s\n", username) fmt.Fprintf(&b, " IdentityFile ~/.ssh/%s\n", keyFileName) fmt.Fprintf(&b, " IdentitiesOnly yes\n") for _, f := range forwards { comment := f.HostName + "/" + f.ServiceName if f.Description != "" { comment += ": " + f.Description } fmt.Fprintf(&b, " LocalForward %d %s:%d # %s\n", f.LocalPort, f.Address, f.Port, comment) } return b.String() } // RenderClientConfigProxyJump is the ProxyJump-mode counterpart of // RenderClientConfig: instead of one "jump" connection carrying a // LocalForward per service, each allowed service gets its own "Host " // block that reaches it by proxying through "jump" (ProxyJump), so the user // runs "ssh " directly per service rather than opening one // always-open tunnel and pointing local tools at 127.0.0.1:. // // Same non-boundary caveat as RenderClientConfig: nothing here is enforced — // the actual restriction to these destinations is authorized_keys // permitopen= (see internal/sysadm/authorizedkeys.go), unaffected by which // client config style is handed out. func RenderClientConfigProxyJump(jumpHost string, jumpPort int, username, keyFileName string, forwards []model.Forward) string { var b strings.Builder fmt.Fprintf(&b, "# ssh-access-tui client config for %s\n", username) fmt.Fprintf(&b, "# Generated %s. Regenerated whenever your allowed services change.\n\n", genTimestamp()) fmt.Fprintf(&b, "Host jump\n") fmt.Fprintf(&b, " HostName %s\n", jumpHost) fmt.Fprintf(&b, " Port %d\n", jumpPort) fmt.Fprintf(&b, " User %s\n", username) fmt.Fprintf(&b, " IdentityFile ~/.ssh/%s\n", keyFileName) fmt.Fprintf(&b, " IdentitiesOnly yes\n\n") aliases := hostAliases(forwards) for i, f := range forwards { fmt.Fprintf(&b, "Host %s\n", aliases[i]) fmt.Fprintf(&b, " HostName %s\n", f.Address) fmt.Fprintf(&b, " Port %d\n", f.Port) fmt.Fprintf(&b, " User %s\n", username) fmt.Fprintf(&b, " ProxyJump jump\n") fmt.Fprintf(&b, " IdentityFile ~/.ssh/%s\n", keyFileName) fmt.Fprintf(&b, " IdentitiesOnly yes\n\n") } return b.String() } // hostAliases assigns each forward a unique ssh_config "Host" alias: just // the host name when it's the only service of that host present, otherwise // "-" so multiple services on one host don't collide. func hostAliases(forwards []model.Forward) []string { countByHost := map[string]int{} for _, f := range forwards { countByHost[f.HostName]++ } out := make([]string, len(forwards)) for i, f := range forwards { if countByHost[f.HostName] > 1 { out[i] = f.HostName + "-" + f.ServiceName } else { out[i] = f.HostName } } return out }