mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-15 19:21:39 +00:00
cmd/derper, derp/derphttp: support, generate self-signed IP address certs
For people who can't use LetsEncrypt because it's banned. Per https://github.com/tailscale/tailscale/issues/11776#issuecomment-2520955317 This does two things: 1) if you run derper with --certmode=manual and --hostname=$IP_ADDRESS we previously permitted, but now we also: * auto-generate the self-signed cert for you if it doesn't yet exist on disk * print out the derpmap configuration you need to use that self-signed cert 2) teaches derp/derphttp's derp dialer to verify the signature of self-signed TLS certs, if so declared in the existing DERPNode.CertName field, which previously existed for domain fronting, separating out the dial hostname from how certs are validates, so it's not overloaded much; that's what it was meant for. Fixes #11776 Change-Id: Ie72d12f209416bb7e8325fe0838cd2c66342c5cf Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
e80d2b4ad1
commit
7fac0175c0
@ -4,16 +4,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
var unsafeHostnameCharacters = regexp.MustCompile(`[^a-zA-Z0-9-\.]`)
|
||||
@ -65,8 +77,18 @@ func NewManualCertManager(certdir, hostname string) (certProvider, error) {
|
||||
crtPath := filepath.Join(certdir, keyname+".crt")
|
||||
keyPath := filepath.Join(certdir, keyname+".key")
|
||||
cert, err := tls.LoadX509KeyPair(crtPath, keyPath)
|
||||
hostnameIP := net.ParseIP(hostname) // or nil if hostname isn't an IP address
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can not load x509 key pair for hostname %q: %w", keyname, err)
|
||||
// If the hostname is an IP address, automatically create a
|
||||
// self-signed certificate for it.
|
||||
var certp *tls.Certificate
|
||||
if os.IsNotExist(err) && hostnameIP != nil {
|
||||
certp, err = createSelfSignedIPCert(crtPath, keyPath, hostname)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can not load x509 key pair for hostname %q: %w", keyname, err)
|
||||
}
|
||||
cert = *certp
|
||||
}
|
||||
// ensure hostname matches with the certificate
|
||||
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
|
||||
@ -76,6 +98,18 @@ func NewManualCertManager(certdir, hostname string) (certProvider, error) {
|
||||
if err := x509Cert.VerifyHostname(hostname); err != nil {
|
||||
return nil, fmt.Errorf("cert invalid for hostname %q: %w", hostname, err)
|
||||
}
|
||||
if hostnameIP != nil {
|
||||
// If the hostname is an IP address, print out information on how to
|
||||
// confgure this in the derpmap.
|
||||
dn := &tailcfg.DERPNode{
|
||||
Name: "custom",
|
||||
RegionID: 900,
|
||||
HostName: hostname,
|
||||
CertName: fmt.Sprintf("sha256-raw:%-02x", sha256.Sum256(x509Cert.Raw)),
|
||||
}
|
||||
dnJSON, _ := json.Marshal(dn)
|
||||
log.Printf("Using self-signed certificate for IP address %q. Configure it in DERPMap using: (https://tailscale.com/s/custom-derp)\n %s", hostname, dnJSON)
|
||||
}
|
||||
return &manualCertManager{
|
||||
cert: &cert,
|
||||
hostname: hostname,
|
||||
@ -109,3 +143,69 @@ func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certif
|
||||
func (m *manualCertManager) HTTPHandler(fallback http.Handler) http.Handler {
|
||||
return fallback
|
||||
}
|
||||
|
||||
func createSelfSignedIPCert(crtPath, keyPath, ipStr string) (*tls.Certificate, error) {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("invalid IP address: %s", ipStr)
|
||||
}
|
||||
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate EC private key: %v", err)
|
||||
}
|
||||
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate serial number: %v", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
CommonName: ipStr,
|
||||
},
|
||||
NotBefore: now,
|
||||
NotAfter: now.AddDate(1, 0, 0), // expires in 1 year; a bit over that is rejected by macOS etc
|
||||
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
// Set the IP as a SAN.
|
||||
template.IPAddresses = []net.IP{ip}
|
||||
|
||||
// Create the self-signed certificate.
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create certificate: %v", err)
|
||||
}
|
||||
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||
|
||||
keyBytes, err := x509.MarshalECPrivateKey(priv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to marshal EC private key: %v", err)
|
||||
}
|
||||
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes})
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(crtPath), 0700); err != nil {
|
||||
return nil, fmt.Errorf("failed to create directory for certificate: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(crtPath, certPEM, 0644); err != nil {
|
||||
return nil, fmt.Errorf("failed to write certificate to %s: %v", crtPath, err)
|
||||
}
|
||||
if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil {
|
||||
return nil, fmt.Errorf("failed to write key to %s: %v", keyPath, err)
|
||||
}
|
||||
|
||||
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create tls.Certificate: %v", err)
|
||||
}
|
||||
return &tlsCert, nil
|
||||
}
|
||||
|
@ -4,19 +4,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// Verify that in --certmode=manual mode, we can use a bare IP address
|
||||
@ -95,3 +105,66 @@ func TestCertIP(t *testing.T) {
|
||||
t.Fatalf("GetCertificate returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
// Test that we can dial a raw IP without using a hostname and without a WebPKI
|
||||
// cert, validating the cert against the signature of the cert in the DERP map's
|
||||
// DERPNode.
|
||||
//
|
||||
// See https://github.com/tailscale/tailscale/issues/11776.
|
||||
func TestPinnedCertRawIP(t *testing.T) {
|
||||
td := t.TempDir()
|
||||
cp, err := NewManualCertManager(td, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("NewManualCertManager: %v", err)
|
||||
}
|
||||
|
||||
cert, err := cp.TLSConfig().GetCertificate(&tls.ClientHelloInfo{
|
||||
ServerName: "127.0.0.1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GetCertificate: %v", err)
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Listen: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
ds := derp.NewServer(key.NewNode(), t.Logf)
|
||||
|
||||
derpHandler := derphttp.Handler(ds)
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/derp", derpHandler)
|
||||
|
||||
var hs http.Server
|
||||
hs.Handler = mux
|
||||
hs.TLSConfig = cp.TLSConfig()
|
||||
go hs.ServeTLS(ln, "", "")
|
||||
|
||||
lnPort := ln.Addr().(*net.TCPAddr).Port
|
||||
|
||||
reg := &tailcfg.DERPRegion{
|
||||
RegionID: 900,
|
||||
Nodes: []*tailcfg.DERPNode{
|
||||
{
|
||||
RegionID: 900,
|
||||
HostName: "127.0.0.1",
|
||||
CertName: fmt.Sprintf("sha256-raw:%-02x", sha256.Sum256(cert.Leaf.Raw)),
|
||||
DERPPort: lnPort,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
netMon := netmon.NewStatic()
|
||||
dc := derphttp.NewRegionClient(key.NewNode(), t.Logf, netMon, func() *tailcfg.DERPRegion {
|
||||
return reg
|
||||
})
|
||||
defer dc.Close()
|
||||
|
||||
_, connClose, _, err := dc.DialRegionTLS(context.Background(), reg)
|
||||
if err != nil {
|
||||
t.Fatalf("DialRegionTLS: %v", err)
|
||||
}
|
||||
defer connClose.Close()
|
||||
}
|
||||
|
@ -652,7 +652,11 @@ func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
|
||||
tlsConf.VerifyConnection = nil
|
||||
}
|
||||
if node.CertName != "" {
|
||||
tlsdial.SetConfigExpectedCert(tlsConf, node.CertName)
|
||||
if suf, ok := strings.CutPrefix(node.CertName, "sha256-raw:"); ok {
|
||||
tlsdial.SetConfigExpectedCertHash(tlsConf, suf)
|
||||
} else {
|
||||
tlsdial.SetConfigExpectedCert(tlsConf, node.CertName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return tls.Client(nc, tlsConf)
|
||||
@ -666,7 +670,7 @@ func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
|
||||
func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tlsConn *tls.Conn, connClose io.Closer, node *tailcfg.DERPNode, err error) {
|
||||
tcpConn, node, err := c.dialRegion(ctx, reg)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, nil, fmt.Errorf("dialRegion(%d): %w", reg.RegionID, err)
|
||||
}
|
||||
done := make(chan bool) // unbuffered
|
||||
defer close(done)
|
||||
@ -741,6 +745,17 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
|
||||
|
||||
nwait := 0
|
||||
startDial := func(dstPrimary, proto string) {
|
||||
dst := cmp.Or(dstPrimary, n.HostName)
|
||||
|
||||
// If dialing an IP address directly, check its address family
|
||||
// and bail out before incrementing nwait.
|
||||
if ip, err := netip.ParseAddr(dst); err == nil {
|
||||
if proto == "tcp4" && ip.Is6() ||
|
||||
proto == "tcp6" && ip.Is4() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
nwait++
|
||||
go func() {
|
||||
if proto == "tcp4" && c.preferIPv6() {
|
||||
@ -755,7 +770,6 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
|
||||
// Start v4 dial
|
||||
}
|
||||
}
|
||||
dst := cmp.Or(dstPrimary, n.HostName)
|
||||
port := "443"
|
||||
if !c.useHTTPS() {
|
||||
port = "3340"
|
||||
|
@ -12,6 +12,7 @@ package tlsdial
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
@ -246,6 +247,46 @@ func SetConfigExpectedCert(c *tls.Config, certDNSName string) {
|
||||
}
|
||||
}
|
||||
|
||||
// SetConfigExpectedCertHash configures c's VerifyPeerCertificate function
|
||||
// to require that exactly 1 cert is presented, and that the hex of its SHA256 hash
|
||||
// is equal to wantFullCertSHA256Hex and that it's a valid cert for c.ServerName.
|
||||
func SetConfigExpectedCertHash(c *tls.Config, wantFullCertSHA256Hex string) {
|
||||
if c.VerifyPeerCertificate != nil {
|
||||
panic("refusing to override tls.Config.VerifyPeerCertificate")
|
||||
}
|
||||
// Set InsecureSkipVerify to prevent crypto/tls from doing its
|
||||
// own cert verification, but do the same work that it'd do
|
||||
// (but using certDNSName) in the VerifyPeerCertificate hook.
|
||||
c.InsecureSkipVerify = true
|
||||
c.VerifyConnection = nil
|
||||
c.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
|
||||
if len(rawCerts) == 0 {
|
||||
return errors.New("no certs presented")
|
||||
}
|
||||
if len(rawCerts) > 1 {
|
||||
return errors.New("unexpected multiple certs presented")
|
||||
}
|
||||
if fmt.Sprintf("%02x", sha256.Sum256(rawCerts[0])) != wantFullCertSHA256Hex {
|
||||
return fmt.Errorf("cert hash does not match expected cert hash")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(rawCerts[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("ParseCertificate: %w", err)
|
||||
}
|
||||
if err := cert.VerifyHostname(c.ServerName); err != nil {
|
||||
return fmt.Errorf("cert does not match server name %q: %w", c.ServerName, err)
|
||||
}
|
||||
now := time.Now()
|
||||
if now.After(cert.NotAfter) {
|
||||
return fmt.Errorf("cert expired %v", cert.NotAfter)
|
||||
}
|
||||
if now.Before(cert.NotBefore) {
|
||||
return fmt.Errorf("cert not yet valid until %v; is your clock correct?", cert.NotBefore)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewTransport returns a new HTTP transport that verifies TLS certs using this
|
||||
// package, including its baked-in LetsEncrypt fallback roots.
|
||||
func NewTransport() *http.Transport {
|
||||
|
@ -139,6 +139,12 @@ type DERPNode struct {
|
||||
// name. If empty, HostName is used. If CertName is non-empty,
|
||||
// HostName is only used for the TCP dial (if IPv4/IPv6 are
|
||||
// not present) + TLS ClientHello.
|
||||
//
|
||||
// As a special case, if CertName starts with "sha256-raw:",
|
||||
// then the rest of the string is a hex-encoded SHA256 of the
|
||||
// cert to expect. This is used for self-signed certs.
|
||||
// In this case, the HostName field will typically be an IP
|
||||
// address literal.
|
||||
CertName string `json:",omitempty"`
|
||||
|
||||
// IPv4 optionally forces an IPv4 address to use, instead of using DNS.
|
||||
|
Loading…
x
Reference in New Issue
Block a user