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
import (
"crypto/tls"
"fmt"
"os"
"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))
}
// Load the specified TLS certificate and build transport credentials
// with it.
// Load the specified TLS certificate.
certPool, err := profile.cert()
if err != nil {
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.
opts := []grpc.DialOption{

View File

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

View File

@ -64,6 +64,8 @@ const (
defaultMaxLogFileSize = 10
defaultMinBackoff = time.Second
defaultMaxBackoff = time.Hour
defaultLetsEncryptDirname = "letsencrypt"
defaultLetsEncryptPort = 80
defaultTorSOCKSPort = 9050
defaultTorDNSHost = "soa.nodes.lightning.directory"
@ -127,8 +129,9 @@ var (
defaultTowerDir = filepath.Join(defaultDataDir, defaultTowerSubDirname)
defaultTLSCertPath = filepath.Join(DefaultLndDir, defaultTLSCertFilename)
defaultTLSKeyPath = filepath.Join(DefaultLndDir, defaultTLSKeyFilename)
defaultTLSCertPath = filepath.Join(DefaultLndDir, defaultTLSCertFilename)
defaultTLSKeyPath = filepath.Join(DefaultLndDir, defaultTLSKeyFilename)
defaultLetsEncryptDir = filepath.Join(DefaultLndDir, defaultLetsEncryptDirname)
defaultBtcdDir = btcutil.AppDataDir("btcd", false)
defaultBtcdRPCCertFile = filepath.Join(defaultBtcdDir, "rpc.cert")
@ -179,6 +182,10 @@ type Config struct {
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"`
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
// loadConfig function. We need to expose the 'raw' strings so the
// command line library can access them.
@ -318,6 +325,8 @@ func DefaultConfig() Config {
DebugLevel: defaultLogLevel,
TLSCertPath: defaultTLSCertPath,
TLSKeyPath: defaultTLSKeyPath,
LetsEncryptDir: defaultLetsEncryptDir,
LetsEncryptPort: defaultLetsEncryptPort,
LogDir: defaultLogDir,
MaxLogFiles: defaultMaxLogFiles,
MaxLogFileSize: defaultMaxLogFileSize,
@ -520,6 +529,9 @@ func ValidateConfig(cfg Config, usageMessage string) (*Config, error) {
lndDir := CleanAndExpandPath(cfg.LndDir)
if lndDir != DefaultLndDir {
cfg.DataDir = filepath.Join(lndDir, defaultDataDirname)
cfg.LetsEncryptDir = filepath.Join(
lndDir, defaultLetsEncryptDirname,
)
cfg.TLSCertPath = filepath.Join(lndDir, defaultTLSCertFilename)
cfg.TLSKeyPath = filepath.Join(lndDir, defaultTLSKeyFilename)
cfg.LogDir = filepath.Join(lndDir, defaultLogDirname)
@ -558,6 +570,7 @@ func ValidateConfig(cfg Config, usageMessage string) (*Config, error) {
cfg.DataDir = CleanAndExpandPath(cfg.DataDir)
cfg.TLSCertPath = CleanAndExpandPath(cfg.TLSCertPath)
cfg.TLSKeyPath = CleanAndExpandPath(cfg.TLSKeyPath)
cfg.LetsEncryptDir = CleanAndExpandPath(cfg.LetsEncryptDir)
cfg.AdminMacPath = CleanAndExpandPath(cfg.AdminMacPath)
cfg.ReadMacPath = CleanAndExpandPath(cfg.ReadMacPath)
cfg.InvoiceMacPath = CleanAndExpandPath(cfg.InvoiceMacPath)

84
lnd.go
View File

@ -23,6 +23,7 @@ import (
"github.com/btcsuite/btcwallet/wallet"
proxy "github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/lightninglabs/neutrino"
"golang.org/x/crypto/acme/autocert"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"gopkg.in/macaroon-bakery.v2/bakery"
@ -264,13 +265,15 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error {
defer cleanUp()
// 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 {
err := fmt.Errorf("unable to load TLS credentials: %v", err)
ltndLog.Error(err)
return err
}
defer cleanUp()
serverCreds := credentials.NewTLS(tlsCfg)
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
// and a proxy destination for the REST reverse proxy.
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.
if !fileExists(cfg.TLSCertPath) && !fileExists(cfg.TLSKeyPath) {
@ -759,7 +762,7 @@ func getTLSConfig(cfg *Config) (*tls.Config, *credentials.TransportCredentials,
cfg.TLSDisableAutofill, cert.DefaultAutogenValidity,
)
if err != nil {
return nil, nil, "", err
return nil, nil, "", nil, err
}
rpcsLog.Infof("Done generating TLS certificates")
}
@ -768,7 +771,7 @@ func getTLSConfig(cfg *Config) (*tls.Config, *credentials.TransportCredentials,
cfg.TLSCertPath, cfg.TLSKeyPath,
)
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
@ -782,7 +785,7 @@ func getTLSConfig(cfg *Config) (*tls.Config, *credentials.TransportCredentials,
cfg.TLSExtraDomains, cfg.TLSDisableAutofill,
)
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)
if err != nil {
return nil, nil, "", err
return nil, nil, "", nil, err
}
err = os.Remove(cfg.TLSKeyPath)
if err != nil {
return nil, nil, "", err
return nil, nil, "", nil, err
}
rpcsLog.Infof("Renewing TLS certificates...")
@ -809,7 +812,7 @@ func getTLSConfig(cfg *Config) (*tls.Config, *credentials.TransportCredentials,
cfg.TLSDisableAutofill, cert.DefaultAutogenValidity,
)
if err != nil {
return nil, nil, "", err
return nil, nil, "", nil, err
}
rpcsLog.Infof("Done renewing TLS certificates")
@ -818,14 +821,15 @@ func getTLSConfig(cfg *Config) (*tls.Config, *credentials.TransportCredentials,
cfg.TLSCertPath, cfg.TLSKeyPath,
)
if err != nil {
return nil, nil, "", err
return nil, nil, "", nil, err
}
}
tlsCfg := cert.TLSConfFromCert(certData)
restCreds, err := credentials.NewClientTLSFromFile(cfg.TLSCertPath, "")
if err != nil {
return nil, nil, "", err
return nil, nil, "", nil, err
}
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.

View File

@ -50,6 +50,20 @@
; or want to expose the node at a domain.
; 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
; authenticate all RPC access. If one wishes to opt out of macaroons, uncomment
; the line below.

View File

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