mirror of
https://github.com/tailscale/tailscale.git
synced 2026-09-22 03:25:16 -04:00
The main server file mixed portable session handling with Unix details: sending SIGHUP to end a session, decoding exec.ExitError, the euid check for whether the process can switch users, agent forwarding's chown of the socket, and reading /etc/ssh host keys as root. Those now sit behind small functions (hangupProcess, waitProcess, canSwitchToLocalUser, handleSSHAgentForwarding, systemHostKeyFile, isRootUser) in the new process_unix.go, along with the incubator's forwarded-environment pipe helpers, and the session's *exec.Cmd moves into an embedded osSessionState struct defined there, so that the portable code no longer refers to the process representation at all. user.go keeps only the portable userMeta and userLookup; the login shell and default PATH logic moves to user_unix.go. The SFTP child entrypoint and its stdio adapter, which incubator.go and incubator_plan9.go each had a copy of, move to sftp.go. The c2n usernames handler gains a hook for platforms that list users some other way than /etc/passwd. The agent socket's uid and gid are parsed as 31-bit rather than 32-bit unsigned values so that the conversion to int for os.Chown cannot overflow on 32-bit platforms, which is the pattern CodeQL flags. Updates #cleanup Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I4c7e2b9a0d3f5e1c8b6a4d2f0e9c7b5a3d1f8e6c
159 lines
4.6 KiB
Go
159 lines
4.6 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build (linux && !android) || (darwin && !ios) || freebsd || openbsd || plan9
|
|
|
|
package tailssh
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ecdsa"
|
|
"crypto/ed25519"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/mak"
|
|
)
|
|
|
|
// keyTypes are the SSH key types that we either try to read from the
|
|
// system's OpenSSH keys or try to generate for ourselves when not
|
|
// running as root.
|
|
var keyTypes = []string{"rsa", "ecdsa", "ed25519"}
|
|
|
|
// getHostKeys returns the SSH host keys, using the system's OpenSSH keys when
|
|
// they are readable (see systemHostKeyFile) and generating Tailscale-specific
|
|
// keys as needed.
|
|
func getHostKeys(varRoot string, logf logger.Logf) ([]ssh.Signer, error) {
|
|
existing := getSystemHostKeys(logf)
|
|
return getTailscaleHostKeys(varRoot, existing)
|
|
}
|
|
|
|
// getHostKeyPublicStrings returns the SSH host key public key strings.
|
|
func getHostKeyPublicStrings(varRoot string, logf logger.Logf) ([]string, error) {
|
|
signers, err := getHostKeys(varRoot, logf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var keyStrings []string
|
|
for _, signer := range signers {
|
|
keyStrings = append(keyStrings, strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey()))))
|
|
}
|
|
return keyStrings, nil
|
|
}
|
|
|
|
// getTailscaleHostKeys returns the three (rsa, ecdsa, ed25519) SSH host
|
|
// keys, reusing the provided ones in existing if present in the map.
|
|
func getTailscaleHostKeys(varRoot string, existing map[string]ssh.Signer) (keys []ssh.Signer, err error) {
|
|
var keyDir string // lazily initialized $TAILSCALE_VAR/ssh dir.
|
|
for _, typ := range keyTypes {
|
|
if s, ok := existing[typ]; ok {
|
|
keys = append(keys, s)
|
|
continue
|
|
}
|
|
if keyDir == "" {
|
|
if varRoot == "" {
|
|
return nil, errors.New("no var root for ssh keys")
|
|
}
|
|
keyDir = filepath.Join(varRoot, "ssh")
|
|
if err := os.MkdirAll(keyDir, 0700); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
hostKey, err := hostKeyFileOrCreate(keyDir, typ)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating SSH host key type %q in %q: %w", typ, keyDir, err)
|
|
}
|
|
signer, err := ssh.ParsePrivateKey(hostKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing SSH host key type %q from %q: %w", typ, keyDir, err)
|
|
}
|
|
keys = append(keys, signer)
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
// keyGenMu protects concurrent generation of host keys with
|
|
// [hostKeyFileOrCreate], making sure two callers don't try to concurrently find
|
|
// a missing key and generate it at the same time, returning different keys to
|
|
// their callers.
|
|
//
|
|
// Technically we actually want to have a mutex per directory (the keyDir
|
|
// passed), but that's overkill for how rarely keys are loaded or generated.
|
|
var keyGenMu sync.Mutex
|
|
|
|
func hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
|
|
keyGenMu.Lock()
|
|
defer keyGenMu.Unlock()
|
|
|
|
path := filepath.Join(keyDir, "ssh_host_"+typ+"_key")
|
|
v, err := os.ReadFile(path)
|
|
if err == nil {
|
|
return v, nil
|
|
}
|
|
if !os.IsNotExist(err) {
|
|
return nil, err
|
|
}
|
|
var priv any
|
|
switch typ {
|
|
default:
|
|
return nil, fmt.Errorf("unsupported key type %q", typ)
|
|
case "ed25519":
|
|
_, priv, err = ed25519.GenerateKey(rand.Reader)
|
|
case "ecdsa":
|
|
// curve is arbitrary. We pick whatever will at
|
|
// least pacify clients as the actual encryption
|
|
// doesn't matter: it's all over WireGuard anyway.
|
|
curve := elliptic.P256()
|
|
priv, err = ecdsa.GenerateKey(curve, rand.Reader)
|
|
case "rsa":
|
|
// keySize is arbitrary. We pick whatever will at
|
|
// least pacify clients as the actual encryption
|
|
// doesn't matter: it's all over WireGuard anyway.
|
|
const keySize = 2048
|
|
priv, err = rsa.GenerateKey(rand.Reader, keySize)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mk, err := x509.MarshalPKCS8PrivateKey(priv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pemGen := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: mk})
|
|
err = os.WriteFile(path, pemGen, 0700)
|
|
return pemGen, err
|
|
}
|
|
|
|
// getSystemHostKeys returns the system's OpenSSH host keys that tailssh can
|
|
// read, keyed by type. It returns nil if there are none.
|
|
func getSystemHostKeys(logf logger.Logf) (ret map[string]ssh.Signer) {
|
|
for _, typ := range keyTypes {
|
|
filename := systemHostKeyFile(typ)
|
|
if filename == "" {
|
|
continue
|
|
}
|
|
hostKey, err := os.ReadFile(filename)
|
|
if err != nil || len(bytes.TrimSpace(hostKey)) == 0 {
|
|
continue
|
|
}
|
|
signer, err := ssh.ParsePrivateKey(hostKey)
|
|
if err != nil {
|
|
logf("warning: error reading host key %s: %v (generating one instead)", filename, err)
|
|
continue
|
|
}
|
|
mak.Set(&ret, typ, signer)
|
|
}
|
|
return ret
|
|
}
|