lnrpc: lets encrypt

This commit enables lnd to request and renew a Let's Encrypt
certificate. This certificate is used both for the grpc as well as the
rest listeners. It allows clients to connect without having a copy of
the (public) server certificate.

Co-authored-by: Vegard Engen <vegard@engen.priv.no>
This commit is contained in:
Joost Jager 2019-01-07 18:48:02 +01:00
parent 999ffffa37
commit 403d72b468
No known key found for this signature in database
GPG Key ID: A61B9D4C393C59C7
6 changed files with 133 additions and 22 deletions

View File

@ -5,6 +5,7 @@
package main package main
import ( import (
"crypto/tls"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -74,13 +75,24 @@ func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
fatal(fmt.Errorf("could not load global options: %v", err)) fatal(fmt.Errorf("could not load global options: %v", err))
} }
// Load the specified TLS certificate and build transport credentials // Load the specified TLS certificate.
// with it.
certPool, err := profile.cert() certPool, err := profile.cert()
if err != nil { if err != nil {
fatal(fmt.Errorf("could not create cert pool: %v", err)) fatal(fmt.Errorf("could not create cert pool: %v", err))
} }
creds := credentials.NewClientTLSFromCert(certPool, "")
// Build transport credentials from the certificate pool. If there is no
// certificate pool, we expect the server to use a non-self-signed
// certificate such as a certificate obtained from Let's Encrypt.
var creds credentials.TransportCredentials
if certPool != nil {
creds = credentials.NewClientTLSFromCert(certPool, "")
} else {
// Fallback to the system pool. Using an empty tls config is an
// alternative to x509.SystemCertPool(). That call is not
// supported on Windows.
creds = credentials.NewTLS(&tls.Config{})
}
// Create a dial options array. // Create a dial options array.
opts := []grpc.DialOption{ opts := []grpc.DialOption{

View File

@ -36,6 +36,10 @@ type profileEntry struct {
// cert returns the profile's TLS certificate as a x509 certificate pool. // cert returns the profile's TLS certificate as a x509 certificate pool.
func (e *profileEntry) cert() (*x509.CertPool, error) { func (e *profileEntry) cert() (*x509.CertPool, error) {
if e.TLSCert == "" {
return nil, nil
}
cp := x509.NewCertPool() cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM([]byte(e.TLSCert)) { if !cp.AppendCertsFromPEM([]byte(e.TLSCert)) {
return nil, fmt.Errorf("credentials: failed to append " + return nil, fmt.Errorf("credentials: failed to append " +
@ -113,11 +117,16 @@ func profileFromContext(ctx *cli.Context, store bool) (*profileEntry, error) {
return nil, err return nil, err
} }
// Load the certificate file now. We store it as plain PEM directly. // Load the certificate file now, if specified. We store it as plain PEM
tlsCert, err := ioutil.ReadFile(tlsCertPath) // directly.
if err != nil { var tlsCert []byte
return nil, fmt.Errorf("could not load TLS cert file %s: %v", if lnrpc.FileExists(tlsCertPath) {
tlsCertPath, err) var err error
tlsCert, err = ioutil.ReadFile(tlsCertPath)
if err != nil {
return nil, fmt.Errorf("could not load TLS cert file "+
"%s: %v", tlsCertPath, err)
}
} }
// Now load and possibly encrypt the macaroon file. // Now load and possibly encrypt the macaroon file.

View File

@ -64,6 +64,8 @@ const (
defaultMaxLogFileSize = 10 defaultMaxLogFileSize = 10
defaultMinBackoff = time.Second defaultMinBackoff = time.Second
defaultMaxBackoff = time.Hour defaultMaxBackoff = time.Hour
defaultLetsEncryptDirname = "letsencrypt"
defaultLetsEncryptPort = 80
defaultTorSOCKSPort = 9050 defaultTorSOCKSPort = 9050
defaultTorDNSHost = "soa.nodes.lightning.directory" defaultTorDNSHost = "soa.nodes.lightning.directory"
@ -127,8 +129,9 @@ var (
defaultTowerDir = filepath.Join(defaultDataDir, defaultTowerSubDirname) defaultTowerDir = filepath.Join(defaultDataDir, defaultTowerSubDirname)
defaultTLSCertPath = filepath.Join(DefaultLndDir, defaultTLSCertFilename) defaultTLSCertPath = filepath.Join(DefaultLndDir, defaultTLSCertFilename)
defaultTLSKeyPath = filepath.Join(DefaultLndDir, defaultTLSKeyFilename) defaultTLSKeyPath = filepath.Join(DefaultLndDir, defaultTLSKeyFilename)
defaultLetsEncryptDir = filepath.Join(DefaultLndDir, defaultLetsEncryptDirname)
defaultBtcdDir = btcutil.AppDataDir("btcd", false) defaultBtcdDir = btcutil.AppDataDir("btcd", false)
defaultBtcdRPCCertFile = filepath.Join(defaultBtcdDir, "rpc.cert") defaultBtcdRPCCertFile = filepath.Join(defaultBtcdDir, "rpc.cert")
@ -179,6 +182,10 @@ type Config struct {
MaxLogFileSize int `long:"maxlogfilesize" description:"Maximum logfile size in MB"` MaxLogFileSize int `long:"maxlogfilesize" description:"Maximum logfile size in MB"`
AcceptorTimeout time.Duration `long:"acceptortimeout" description:"Time after which an RPCAcceptor will time out and return false if it hasn't yet received a response"` AcceptorTimeout time.Duration `long:"acceptortimeout" description:"Time after which an RPCAcceptor will time out and return false if it hasn't yet received a response"`
LetsEncryptDir string `long:"letsencryptdir" description:"The directory to store Let's Encrypt certificates within"`
LetsEncryptPort int `long:"letsencryptport" description:"The port on which lnd will listen for Let's Encrypt challenges. Let's Encrypt will always try to contact on port 80. Often non-root processes are not allowed to bind to ports lower than 1024. This configuration option allows a different port to be used, but must be used in combination with port forwarding from port 80."`
LetsEncryptDomain string `long:"letsencryptdomain" description:"Request a Let's Encrypt certificate for this domain. Note that the certicate is only requested and stored when the first rpc connection comes in."`
// We'll parse these 'raw' string arguments into real net.Addrs in the // We'll parse these 'raw' string arguments into real net.Addrs in the
// loadConfig function. We need to expose the 'raw' strings so the // loadConfig function. We need to expose the 'raw' strings so the
// command line library can access them. // command line library can access them.
@ -318,6 +325,8 @@ func DefaultConfig() Config {
DebugLevel: defaultLogLevel, DebugLevel: defaultLogLevel,
TLSCertPath: defaultTLSCertPath, TLSCertPath: defaultTLSCertPath,
TLSKeyPath: defaultTLSKeyPath, TLSKeyPath: defaultTLSKeyPath,
LetsEncryptDir: defaultLetsEncryptDir,
LetsEncryptPort: defaultLetsEncryptPort,
LogDir: defaultLogDir, LogDir: defaultLogDir,
MaxLogFiles: defaultMaxLogFiles, MaxLogFiles: defaultMaxLogFiles,
MaxLogFileSize: defaultMaxLogFileSize, MaxLogFileSize: defaultMaxLogFileSize,
@ -520,6 +529,9 @@ func ValidateConfig(cfg Config, usageMessage string) (*Config, error) {
lndDir := CleanAndExpandPath(cfg.LndDir) lndDir := CleanAndExpandPath(cfg.LndDir)
if lndDir != DefaultLndDir { if lndDir != DefaultLndDir {
cfg.DataDir = filepath.Join(lndDir, defaultDataDirname) cfg.DataDir = filepath.Join(lndDir, defaultDataDirname)
cfg.LetsEncryptDir = filepath.Join(
lndDir, defaultLetsEncryptDirname,
)
cfg.TLSCertPath = filepath.Join(lndDir, defaultTLSCertFilename) cfg.TLSCertPath = filepath.Join(lndDir, defaultTLSCertFilename)
cfg.TLSKeyPath = filepath.Join(lndDir, defaultTLSKeyFilename) cfg.TLSKeyPath = filepath.Join(lndDir, defaultTLSKeyFilename)
cfg.LogDir = filepath.Join(lndDir, defaultLogDirname) cfg.LogDir = filepath.Join(lndDir, defaultLogDirname)
@ -558,6 +570,7 @@ func ValidateConfig(cfg Config, usageMessage string) (*Config, error) {
cfg.DataDir = CleanAndExpandPath(cfg.DataDir) cfg.DataDir = CleanAndExpandPath(cfg.DataDir)
cfg.TLSCertPath = CleanAndExpandPath(cfg.TLSCertPath) cfg.TLSCertPath = CleanAndExpandPath(cfg.TLSCertPath)
cfg.TLSKeyPath = CleanAndExpandPath(cfg.TLSKeyPath) cfg.TLSKeyPath = CleanAndExpandPath(cfg.TLSKeyPath)
cfg.LetsEncryptDir = CleanAndExpandPath(cfg.LetsEncryptDir)
cfg.AdminMacPath = CleanAndExpandPath(cfg.AdminMacPath) cfg.AdminMacPath = CleanAndExpandPath(cfg.AdminMacPath)
cfg.ReadMacPath = CleanAndExpandPath(cfg.ReadMacPath) cfg.ReadMacPath = CleanAndExpandPath(cfg.ReadMacPath)
cfg.InvoiceMacPath = CleanAndExpandPath(cfg.InvoiceMacPath) cfg.InvoiceMacPath = CleanAndExpandPath(cfg.InvoiceMacPath)

84
lnd.go
View File

@ -23,6 +23,7 @@ import (
"github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/wallet"
proxy "github.com/grpc-ecosystem/grpc-gateway/runtime" proxy "github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/lightninglabs/neutrino" "github.com/lightninglabs/neutrino"
"golang.org/x/crypto/acme/autocert"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials"
"gopkg.in/macaroon-bakery.v2/bakery" "gopkg.in/macaroon-bakery.v2/bakery"
@ -264,13 +265,15 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
defer cleanUp() defer cleanUp()
// Only process macaroons if --no-macaroons isn't set. // Only process macaroons if --no-macaroons isn't set.
tlsCfg, restCreds, restProxyDest, err := getTLSConfig(cfg) tlsCfg, restCreds, restProxyDest, cleanUp, err := getTLSConfig(cfg)
if err != nil { if err != nil {
err := fmt.Errorf("unable to load TLS credentials: %v", err) err := fmt.Errorf("unable to load TLS credentials: %v", err)
ltndLog.Error(err) ltndLog.Error(err)
return err return err
} }
defer cleanUp()
serverCreds := credentials.NewTLS(tlsCfg) serverCreds := credentials.NewTLS(tlsCfg)
serverOpts := []grpc.ServerOption{grpc.Creds(serverCreds)} serverOpts := []grpc.ServerOption{grpc.Creds(serverCreds)}
@ -748,7 +751,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
// getTLSConfig returns a TLS configuration for the gRPC server and credentials // getTLSConfig returns a TLS configuration for the gRPC server and credentials
// and a proxy destination for the REST reverse proxy. // and a proxy destination for the REST reverse proxy.
func getTLSConfig(cfg *Config) (*tls.Config, *credentials.TransportCredentials, func getTLSConfig(cfg *Config) (*tls.Config, *credentials.TransportCredentials,
string, error) { string, func(), error) {
// Ensure we create TLS key and certificate if they don't exist. // Ensure we create TLS key and certificate if they don't exist.
if !fileExists(cfg.TLSCertPath) && !fileExists(cfg.TLSKeyPath) { if !fileExists(cfg.TLSCertPath) && !fileExists(cfg.TLSKeyPath) {
@ -759,7 +762,7 @@ func getTLSConfig(cfg *Config) (*tls.Config, *credentials.TransportCredentials,
cfg.TLSDisableAutofill, cert.DefaultAutogenValidity, cfg.TLSDisableAutofill, cert.DefaultAutogenValidity,
) )
if err != nil { if err != nil {
return nil, nil, "", err return nil, nil, "", nil, err
} }
rpcsLog.Infof("Done generating TLS certificates") rpcsLog.Infof("Done generating TLS certificates")
} }
@ -768,7 +771,7 @@ func getTLSConfig(cfg *Config) (*tls.Config, *credentials.TransportCredentials,
cfg.TLSCertPath, cfg.TLSKeyPath, cfg.TLSCertPath, cfg.TLSKeyPath,
) )
if err != nil { if err != nil {
return nil, nil, "", err return nil, nil, "", nil, err
} }
// We check whether the certifcate we have on disk match the IPs and // We check whether the certifcate we have on disk match the IPs and
@ -782,7 +785,7 @@ func getTLSConfig(cfg *Config) (*tls.Config, *credentials.TransportCredentials,
cfg.TLSExtraDomains, cfg.TLSDisableAutofill, cfg.TLSExtraDomains, cfg.TLSDisableAutofill,
) )
if err != nil { if err != nil {
return nil, nil, "", err return nil, nil, "", nil, err
} }
} }
@ -794,12 +797,12 @@ func getTLSConfig(cfg *Config) (*tls.Config, *credentials.TransportCredentials,
err := os.Remove(cfg.TLSCertPath) err := os.Remove(cfg.TLSCertPath)
if err != nil { if err != nil {
return nil, nil, "", err return nil, nil, "", nil, err
} }
err = os.Remove(cfg.TLSKeyPath) err = os.Remove(cfg.TLSKeyPath)
if err != nil { if err != nil {
return nil, nil, "", err return nil, nil, "", nil, err
} }
rpcsLog.Infof("Renewing TLS certificates...") rpcsLog.Infof("Renewing TLS certificates...")
@ -809,7 +812,7 @@ func getTLSConfig(cfg *Config) (*tls.Config, *credentials.TransportCredentials,
cfg.TLSDisableAutofill, cert.DefaultAutogenValidity, cfg.TLSDisableAutofill, cert.DefaultAutogenValidity,
) )
if err != nil { if err != nil {
return nil, nil, "", err return nil, nil, "", nil, err
} }
rpcsLog.Infof("Done renewing TLS certificates") rpcsLog.Infof("Done renewing TLS certificates")
@ -818,14 +821,15 @@ func getTLSConfig(cfg *Config) (*tls.Config, *credentials.TransportCredentials,
cfg.TLSCertPath, cfg.TLSKeyPath, cfg.TLSCertPath, cfg.TLSKeyPath,
) )
if err != nil { if err != nil {
return nil, nil, "", err return nil, nil, "", nil, err
} }
} }
tlsCfg := cert.TLSConfFromCert(certData) tlsCfg := cert.TLSConfFromCert(certData)
restCreds, err := credentials.NewClientTLSFromFile(cfg.TLSCertPath, "") restCreds, err := credentials.NewClientTLSFromFile(cfg.TLSCertPath, "")
if err != nil { if err != nil {
return nil, nil, "", err return nil, nil, "", nil, err
} }
restProxyDest := cfg.RPCListeners[0].String() restProxyDest := cfg.RPCListeners[0].String()
@ -841,7 +845,65 @@ func getTLSConfig(cfg *Config) (*tls.Config, *credentials.TransportCredentials,
) )
} }
return tlsCfg, &restCreds, restProxyDest, nil // If Let's Encrypt is enabled, instantiate autocert to request/renew
// the certificates.
cleanUp := func() {}
if cfg.LetsEncryptDomain != "" {
ltndLog.Infof("Using Let's Encrypt certificate for domain %v",
cfg.LetsEncryptDomain)
manager := autocert.Manager{
Cache: autocert.DirCache(cfg.LetsEncryptDir),
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(cfg.LetsEncryptDomain),
}
addr := fmt.Sprintf(":%v", cfg.LetsEncryptPort)
srv := &http.Server{
Addr: addr,
Handler: manager.HTTPHandler(nil),
}
shutdownCompleted := make(chan struct{})
cleanUp = func() {
err := srv.Shutdown(context.Background())
if err != nil {
ltndLog.Errorf("Autocert listener shutdown "+
" error: %v", err)
return
}
<-shutdownCompleted
ltndLog.Infof("Autocert challenge listener stopped")
}
go func() {
ltndLog.Infof("Autocert challenge listener started "+
"at %v", addr)
err := srv.ListenAndServe()
if err != http.ErrServerClosed {
ltndLog.Errorf("autocert http: %v", err)
}
close(shutdownCompleted)
}()
getCertificate := func(h *tls.ClientHelloInfo) (
*tls.Certificate, error) {
lecert, err := manager.GetCertificate(h)
if err != nil {
ltndLog.Errorf("GetCertificate: %v", err)
return &certData, nil
}
return lecert, err
}
// The self-signed tls.cert remains available as fallback.
tlsCfg.GetCertificate = getCertificate
}
return tlsCfg, &restCreds, restProxyDest, cleanUp, nil
} }
// fileExists reports whether the named file or directory exists. // fileExists reports whether the named file or directory exists.

View File

@ -50,6 +50,20 @@
; or want to expose the node at a domain. ; or want to expose the node at a domain.
; externalhosts=my-node-domain.com ; externalhosts=my-node-domain.com
; Sets the directory to store Let's Encrypt certificates within
; letsencryptdir=~/.lnd/letsencrypt
; Sets the port on which lnd will listen for Let's Encrypt challenges. Let's
; Encrypt will always try to contact on port 80. Often non-root processes are
; not allowed to bind to ports lower than 1024. This configuration option allows
; a different port to be used, but must be used in combination with port
; forwarding from port 80.
; letsencryptport=8080
; Request a Let's Encrypt certificate for this domain. Note that the certicate
; is only requested and stored when the first rpc connection comes in.
; letsencryptdomain=example.com
; Disable macaroon authentication. Macaroons are used are bearer credentials to ; Disable macaroon authentication. Macaroons are used are bearer credentials to
; authenticate all RPC access. If one wishes to opt out of macaroons, uncomment ; authenticate all RPC access. If one wishes to opt out of macaroons, uncomment
; the line below. ; the line below.

View File

@ -119,10 +119,11 @@ func TestTLSAutoRegeneration(t *testing.T) {
TLSKeyPath: keyPath, TLSKeyPath: keyPath,
RPCListeners: rpcListeners, RPCListeners: rpcListeners,
} }
_, _, _, err = getTLSConfig(cfg) _, _, _, cleanUp, err := getTLSConfig(cfg)
if err != nil { if err != nil {
t.Fatalf("couldn't retrieve TLS config") t.Fatalf("couldn't retrieve TLS config")
} }
defer cleanUp()
// Grab the certificate to test that getTLSConfig did its job correctly // Grab the certificate to test that getTLSConfig did its job correctly
// and generated a new cert. // and generated a new cert.