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:
parent
999ffffa37
commit
403d72b468
@ -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{
|
||||
|
@ -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.
|
||||
|
17
config.go
17
config.go
@ -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
84
lnd.go
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user