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"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/acme/autocert"
|
|
||||||
"tailscale.com/atomicfile"
|
"tailscale.com/atomicfile"
|
||||||
"tailscale.com/derp"
|
"tailscale.com/derp"
|
||||||
"tailscale.com/derp/derphttp"
|
"tailscale.com/derp/derphttp"
|
||||||
@ -39,6 +38,7 @@
|
|||||||
dev = flag.Bool("dev", false, "run in localhost development mode")
|
dev = flag.Bool("dev", false, "run in localhost development mode")
|
||||||
addr = flag.String("a", ":443", "server address")
|
addr = flag.String("a", ":443", "server address")
|
||||||
configPath = flag.String("c", "", "config file path")
|
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")
|
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")
|
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")
|
logCollection = flag.String("logcollection", "", "If non-empty, logtail collection to log to")
|
||||||
@ -130,7 +130,7 @@ func main() {
|
|||||||
|
|
||||||
cfg := loadConfig()
|
cfg := loadConfig()
|
||||||
|
|
||||||
letsEncrypt := tsweb.IsProd443(*addr)
|
serveTLS := tsweb.IsProd443(*addr)
|
||||||
|
|
||||||
s := derp.NewServer(key.Private(cfg.PrivateKey), log.Printf)
|
s := derp.NewServer(key.Private(cfg.PrivateKey), log.Printf)
|
||||||
s.SetVerifyClient(*verifyClients)
|
s.SetVerifyClient(*verifyClients)
|
||||||
@ -204,24 +204,16 @@ func main() {
|
|||||||
WriteTimeout: 30 * time.Second,
|
WriteTimeout: 30 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
if letsEncrypt {
|
if serveTLS {
|
||||||
if *certDir == "" {
|
|
||||||
log.Fatalf("missing required --certdir flag")
|
|
||||||
}
|
|
||||||
log.Printf("derper: serving on %s with TLS", *addr)
|
log.Printf("derper: serving on %s with TLS", *addr)
|
||||||
certManager := &autocert.Manager{
|
certManager, err := certProviderByCertMode(*certMode, *certDir, *hostname)
|
||||||
Prompt: autocert.AcceptTOS,
|
if err != nil {
|
||||||
HostPolicy: autocert.HostWhitelist(*hostname),
|
log.Fatalf("derper: can not start cert provider: %v", err)
|
||||||
Cache: autocert.DirCache(*certDir),
|
|
||||||
}
|
|
||||||
if *hostname == "derp.tailscale.com" {
|
|
||||||
certManager.HostPolicy = prodAutocertHostPolicy
|
|
||||||
certManager.Email = "security@tailscale.com"
|
|
||||||
}
|
}
|
||||||
httpsrv.TLSConfig = certManager.TLSConfig()
|
httpsrv.TLSConfig = certManager.TLSConfig()
|
||||||
letsEncryptGetCert := httpsrv.TLSConfig.GetCertificate
|
getCert := httpsrv.TLSConfig.GetCertificate
|
||||||
httpsrv.TLSConfig.GetCertificate = func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
httpsrv.TLSConfig.GetCertificate = func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
cert, err := letsEncryptGetCert(hi)
|
cert, err := getCert(hi)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user