Something went wrong. Try again.
forked from tailscale/tailscale
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162// 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}) // Private host keys are for tailscaled only: no group/other access, // so 0600 rather than 0700 (which would needlessly allow the key to // be walked into). err = os.WriteFile(path, pemGen, 0600) 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}