ipn/ipnlocal: generate tailscaled-owned SSH keys as needed

Updates #3802

Change-Id: Ie1bc9ae3f3639603b88b4e19b7eb12bea528ff77
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
pull/4028/head
Brad Fitzpatrick 2 years ago committed by Brad Fitzpatrick
parent cce6aad6c0
commit c4a6d9fa5d

@ -8,10 +8,20 @@
package ipnlocal
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"golang.org/x/crypto/ssh"
"tailscale.com/envknob"
@ -19,17 +29,94 @@ import (
var useHostKeys = envknob.Bool("TS_USE_SYSTEM_SSH_HOST_KEYS")
func (b *LocalBackend) GetSSH_HostKeys() ([]ssh.Signer, error) {
// TODO(bradfitz): generate host keys, at least as needed if
// an existing SSH server didn't put them on disk. But also
// because people may want tailscale-specific ones. For now be
// lazy and reuse the host ones.
return b.getSystemSSH_HostKeys()
// 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"}
func (b *LocalBackend) GetSSH_HostKeys() (keys []ssh.Signer, err error) {
if os.Geteuid() == 0 {
keys, err = b.getSystemSSH_HostKeys()
if err != nil || len(keys) > 0 {
return keys, err
}
// Otherwise, perhaps they don't have OpenSSH etc installed.
// Generate our own keys...
}
return b.getTailscaleSSH_HostKeys()
}
func (b *LocalBackend) getTailscaleSSH_HostKeys() (keys []ssh.Signer, err error) {
root := b.TailscaleVarRoot()
if root == "" {
return nil, errors.New("no var root for ssh keys")
}
keyDir := filepath.Join(root, "ssh")
if err := os.MkdirAll(keyDir, 0700); err != nil {
return nil, err
}
for _, typ := range keyTypes {
hostKey, err := b.hostKeyFileOrCreate(keyDir, typ)
if err != nil {
return nil, err
}
signer, err := ssh.ParsePrivateKey(hostKey)
if err != nil {
return nil, err
}
keys = append(keys, signer)
}
return keys, nil
}
var keyGenMu sync.Mutex
func (b *LocalBackend) hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
keyGenMu.Lock()
defer keyGenMu.Unlock()
path := filepath.Join(keyDir, "ssh_host_"+typ+"_key")
v, err := ioutil.ReadFile(path)
if err == nil {
return v, nil
}
if !os.IsNotExist(err) {
return nil, err
}
var priv interface{}
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
}
func (b *LocalBackend) getSystemSSH_HostKeys() (ret []ssh.Signer, err error) {
// TODO(bradfitz): cache this?
for _, typ := range []string{"rsa", "ecdsa", "ed25519"} {
for _, typ := range keyTypes {
hostKey, err := ioutil.ReadFile("/etc/ssh/ssh_host_" + typ + "_key")
if os.IsNotExist(err) {
continue
@ -43,9 +130,6 @@ func (b *LocalBackend) getSystemSSH_HostKeys() (ret []ssh.Signer, err error) {
}
ret = append(ret, signer)
}
if len(ret) == 0 {
return nil, errors.New("no system SSH host keys found")
}
return ret, nil
}

@ -0,0 +1,42 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux
// +build linux
package ipnlocal
import (
"reflect"
"testing"
)
func TestSSHKeyGen(t *testing.T) {
dir := t.TempDir()
lb := &LocalBackend{varRoot: dir}
keys, err := lb.getTailscaleSSH_HostKeys()
if err != nil {
t.Fatal(err)
}
got := map[string]bool{}
for _, k := range keys {
got[k.PublicKey().Type()] = true
}
want := map[string]bool{
"ssh-rsa": true,
"ecdsa-sha2-nistp256": true,
"ssh-ed25519": true,
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("keys = %v; want %v", got, want)
}
keys2, err := lb.getTailscaleSSH_HostKeys()
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(keys, keys2) {
t.Errorf("got different keys on second call")
}
}
Loading…
Cancel
Save