mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 13:05:46 +00:00
cmd/derper: support manual TLS certificate mode (#2793)
Add a mode control for derp server, and add a "manual" mode to get derp server certificate. Under manual mode, certificate is searched in the directory given by "--cert-dir". Certificate should in PEM format, and use "hostname.{key,crt}" as filename. If no hostname is used, search by the hostname given for listen. Fixes #2794 Signed-off-by: SilverBut <SilverBut@users.noreply.github.com>
This commit is contained in:
parent
de63e85810
commit
d8c5d00ecb
95
cmd/derper/cert.go
Normal file
95
cmd/derper/cert.go
Normal file
@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2021 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
var unsafeHostnameCharacters = regexp.MustCompile(`[^a-zA-Z0-9-\.]`)
|
||||
|
||||
type certProvider interface {
|
||||
// TLSConfig creates a new TLS config suitable for net/http.Server servers.
|
||||
TLSConfig() *tls.Config
|
||||
// HTTPHandler handle ACME related request, if any.
|
||||
HTTPHandler(fallback http.Handler) http.Handler
|
||||
}
|
||||
|
||||
func certProviderByCertMode(mode, dir, hostname string) (certProvider, error) {
|
||||
if dir == "" {
|
||||
return nil, errors.New("missing required --certdir flag")
|
||||
}
|
||||
switch mode {
|
||||
case "letsencrypt":
|
||||
certManager := &autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
HostPolicy: autocert.HostWhitelist(hostname),
|
||||
Cache: autocert.DirCache(dir),
|
||||
}
|
||||
if hostname == "derp.tailscale.com" {
|
||||
certManager.HostPolicy = prodAutocertHostPolicy
|
||||
certManager.Email = "security@tailscale.com"
|
||||
}
|
||||
return certManager, nil
|
||||
case "manual":
|
||||
return NewManualCertManager(dir, hostname)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupport cert mode: %q", mode)
|
||||
}
|
||||
}
|
||||
|
||||
type manualCertManager struct {
|
||||
cert *tls.Certificate
|
||||
hostname string
|
||||
}
|
||||
|
||||
// NewManualCertManager returns a cert provider which read certificate by given hostname on create.
|
||||
func NewManualCertManager(certdir, hostname string) (certProvider, error) {
|
||||
keyname := unsafeHostnameCharacters.ReplaceAllString(hostname, "")
|
||||
crtPath := filepath.Join(certdir, keyname+".crt")
|
||||
keyPath := filepath.Join(certdir, keyname+".key")
|
||||
cert, err := tls.LoadX509KeyPair(crtPath, keyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can not load x509 key pair for hostname %q: %w", keyname, err)
|
||||
}
|
||||
// ensure hostname matches with the certificate
|
||||
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can not load cert: %w", err)
|
||||
}
|
||||
if x509Cert.VerifyHostname(hostname) != nil {
|
||||
return nil, errors.New("refuse to load cert: hostname mismatch with key")
|
||||
}
|
||||
return &manualCertManager{cert: &cert, hostname: hostname}, nil
|
||||
}
|
||||
|
||||
func (m *manualCertManager) TLSConfig() *tls.Config {
|
||||
return &tls.Config{
|
||||
Certificates: nil,
|
||||
NextProtos: []string{
|
||||
"h2", "http/1.1", // enable HTTP/2
|
||||
},
|
||||
GetCertificate: m.getCertificate,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if hi.ServerName != m.hostname {
|
||||
return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName)
|
||||
}
|
||||
return m.cert, nil
|
||||
}
|
||||
|
||||
func (m *manualCertManager) HTTPHandler(fallback http.Handler) http.Handler {
|
||||
return fallback
|
||||
}
|
@ -23,7 +23,6 @@
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
@ -39,6 +38,7 @@
|
||||
dev = flag.Bool("dev", false, "run in localhost development mode")
|
||||
addr = flag.String("a", ":443", "server address")
|
||||
configPath = flag.String("c", "", "config file path")
|
||||
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt")
|
||||
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
|
||||
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443")
|
||||
logCollection = flag.String("logcollection", "", "If non-empty, logtail collection to log to")
|
||||
@ -130,7 +130,7 @@ func main() {
|
||||
|
||||
cfg := loadConfig()
|
||||
|
||||
letsEncrypt := tsweb.IsProd443(*addr)
|
||||
serveTLS := tsweb.IsProd443(*addr)
|
||||
|
||||
s := derp.NewServer(key.Private(cfg.PrivateKey), log.Printf)
|
||||
s.SetVerifyClient(*verifyClients)
|
||||
@ -204,24 +204,16 @@ func main() {
|
||||
WriteTimeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
if letsEncrypt {
|
||||
if *certDir == "" {
|
||||
log.Fatalf("missing required --certdir flag")
|
||||
}
|
||||
if serveTLS {
|
||||
log.Printf("derper: serving on %s with TLS", *addr)
|
||||
certManager := &autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
HostPolicy: autocert.HostWhitelist(*hostname),
|
||||
Cache: autocert.DirCache(*certDir),
|
||||
}
|
||||
if *hostname == "derp.tailscale.com" {
|
||||
certManager.HostPolicy = prodAutocertHostPolicy
|
||||
certManager.Email = "security@tailscale.com"
|
||||
certManager, err := certProviderByCertMode(*certMode, *certDir, *hostname)
|
||||
if err != nil {
|
||||
log.Fatalf("derper: can not start cert provider: %v", err)
|
||||
}
|
||||
httpsrv.TLSConfig = certManager.TLSConfig()
|
||||
letsEncryptGetCert := httpsrv.TLSConfig.GetCertificate
|
||||
getCert := httpsrv.TLSConfig.GetCertificate
|
||||
httpsrv.TLSConfig.GetCertificate = func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
cert, err := letsEncryptGetCert(hi)
|
||||
cert, err := getCert(hi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user