Repository: lightningnetwork/lnd (Go)

Issue:
[bug]: Letsencrypt cert is not obtained for lnd versions >=0.16.0
### Background

lnd versions >=0.16.0 don't obtain a letsencrypt cert when configs are set to obtain one. I see this in regtest mode. I have not tried it on testnet or mainnet.

### Your environment

* version of `lnd`: \>=0.16.0

* which operating system: lightning labs docker image - lightninglabs/lnd:v0.16.2-beta

* version of `btcd`, `bitcoind`, or other backend: docker - ruimarinho/bitcoin-core:latest

* any other relevant environment details: tested only in regtest so far

### Steps to reproduce

I have a minimum docker example [here](https://github.com/LukasBahrenberg/lnd-letsencrypt-test.git) with my configs illustrating how a letsencrypt cert is obtained in case of v15 but not for v16. 

### Expected behaviour

I expect lnd to obtain the letsencrypt cert during/after the first REST/RPC call given the configs I have.

### Actual behaviour

The letsencrypt cert is not obtained for lnd >=0.16.0 during/after the first REST/RPC call. 

Instead, the call fails complaining about a self-signed certificate. Does lnd default to the self-signed TLS after the letsencrypt challenge fails?

Testing this is complicated by the fact that also for v15 the letsencrypt challenge only works like 5 times in a row when redeploying lnd from scratch (`docker compose down && docker compose up --build`). After that, REST calls to lnd time out. I can then change the URL for which to obtain the cert and it works again but only so many times. Then even changing the URL does not work anymore at some point. Is this related to letsencrypt limiting requests for URLs/IPs?



Discussion:
Hey!

I'm going to try to reproduce it with the setup you provided and will be looking into it.
I think I can confirm that it is indeed a bug introduced with lnd v0.16. I wasn't really able to reproduce the bug as I didn't really get a good setup for the autocert generation so far. However I was able to confirm that the autocert manager was never called in lnd0.16.2.

I have a hacky fix up at https://github.com/sputn1ck/lnd/tree/autocert_fix could you try running you example with that?
Thanks for taking this on @sputn1ck! Unfortunately, I still see the same behavior as in 16.2. I integrated your fix in my [test repo](https://github.com/LukasBahrenberg/lnd-letsencrypt-test.git) for this. I collected some of the output logs in this [text file](https://github.com/LukasBahrenberg/lnd-letsencrypt-test/blob/2a0218e4b0087dc296793cecd2cfeec45c2095b3/test_results.txt) for comparison. All the logs pretty much show the same in terms of letsencrypt settings. Only the order is slightly different for 15.5 which is where the letsencrypt cert is successfully obtained. 
Hmm I got my test setup working now, for me it now creates the acme files and I get the getinfo call working. I think you didn't build the correct lnd, as my version logs this:
```
2023-05-31 15:39:31.546 [INF] LTND: Version: 0.16.2-beta commit=v0.16.2-beta-dirty, build=production, logging=default, debuglevel=info
2023-05-31 15:39:31.546 [INF] LTND: Active chain: Bitcoin (network=regtest)
2023-05-31 15:39:31.547 [INF] LTND: Using Let's Encrypt certificate for domain ????
2023-05-31 15:39:31.547 [INF] SRVR: set auto acme cert
2023-05-31 15:39:31.547 [INF] LTND: Autocert challenge listener started at 0.0.0.0:80
```

`2023-05-31 15:39:31.547 [INF] SRVR: set auto acme cert` that line should be there if you run my version

`lnd       | 2023-05-31 12:55:00.714 [INF] LTND: Version: 0.16.99-beta commit=v0.16.0-beta-358-gf9d4600ff,` in your logs also differs from my version log
Yeah sorry about the confusion, you are right about the build. It works with your fix!! I was not building the right docker image. Had to look into lnd's docker setup again.

Thanks again!
Cool, I'm going to clean it up then and open a PR!



Patch:
diff --git a/docs/release-notes/release-notes-0.17.0.md b/docs/release-notes/release-notes-0.17.0.md
index 5d177dede79..0a7b64258b8 100644
--- a/docs/release-notes/release-notes-0.17.0.md
+++ b/docs/release-notes/release-notes-0.17.0.md
@@ -164,6 +164,8 @@ unlock or create.
 
 * [Stop rejecting payments that overpay or over-timelock the final hop](https://github.com/lightningnetwork/lnd/pull/7768)
 
+* [Fix let's encrypt autocert generation](https://github.com/lightningnetwork/lnd/pull/7739)
+
 ### Tooling and documentation
 
 * Add support for [custom `RPCHOST` and
@@ -190,6 +192,7 @@ unlock or create.
 * hieblmi
 * Jordi Montes
 * Keagan McClelland
+* Konstantin Nick
 * Lele Calo
 * Matt Morehouse
 * Maxwell Sayles
diff --git a/tls_manager.go b/tls_manager.go
index 85defe8375d..577d2fc6dbb 100644
--- a/tls_manager.go
+++ b/tls_manager.go
@@ -94,7 +94,7 @@ func NewTLSManager(cfg *TLSManagerCfg) *TLSManager {
 // getConfig returns a TLS configuration for the gRPC server and credentials
 // and a proxy destination for the REST reverse proxy.
 func (t *TLSManager) getConfig() ([]grpc.ServerOption, []grpc.DialOption,
-	func(net.Addr) (net.Listener, error), error) {
+	func(net.Addr) (net.Listener, error), func(), error) {
 
 	var (
 		keyBytes, certBytes []byte
@@ -108,19 +108,19 @@ func (t *TLSManager) getConfig() ([]grpc.ServerOption, []grpc.DialOption,
 			t.cfg.TLSCertPath, t.cfg.TLSKeyPath,
 		)
 		if err != nil {
-			return nil, nil, nil, err
+			return nil, nil, nil, nil, err
 		}
 	}
 
 	certData, _, err := cert.LoadCertFromBytes(certBytes, keyBytes)
 	if err != nil {
-		return nil, nil, nil, err
+		return nil, nil, nil, nil, err
 	}
 
 	if t.tlsReloader == nil {
 		tlsr, err := cert.NewTLSReloader(certBytes, keyBytes)
 		if err != nil {
-			return nil, nil, nil, err
+			return nil, nil, nil, nil, err
 		}
 		t.tlsReloader = tlsr
 	}
@@ -128,6 +128,10 @@ func (t *TLSManager) getConfig() ([]grpc.ServerOption, []grpc.DialOption,
 	tlsCfg := cert.TLSConfFromCert(certData)
 	tlsCfg.GetCertificate = t.tlsReloader.GetCertificateFunc()
 
+	// If Let's Encrypt is enabled, we need to set up the autocert manager
+	// and override the TLS config's GetCertificate function.
+	cleanUp := t.setUpLetsEncrypt(&certData, tlsCfg)
+
 	// If we're using the ephemeral certificate, we need to use the
 	// ephemeral cert path.
 	certPath := t.cfg.TLSCertPath
@@ -141,7 +145,7 @@ func (t *TLSManager) getConfig() ([]grpc.ServerOption, []grpc.DialOption,
 		certPath, "",
 	)
 	if err != nil {
-		return nil, nil, nil, err
+		return nil, nil, nil, nil, err
 	}
 
 	serverCreds := credentials.NewTLS(tlsCfg)
@@ -171,40 +175,39 @@ func (t *TLSManager) getConfig() ([]grpc.ServerOption, []grpc.DialOption,
 		return lncfg.TLSListenOnAddress(addr, tlsCfg)
 	}
 
-	return serverOpts, restDialOpts, restListen, nil
+	return serverOpts, restDialOpts, restListen, cleanUp, nil
 }
 
 // generateOrRenewCert generates a new TLS certificate if we're not using one
 // yet or renews it if it's outdated.
-func (t *TLSManager) generateOrRenewCert() (*tls.Config, func(), error) {
+func (t *TLSManager) generateOrRenewCert() (*tls.Config, error) {
 	// Generete a TLS pair if we don't have one yet.
 	var emptyKeyRing keychain.SecretKeyRing
 	err := t.generateCertPair(emptyKeyRing)
 	if err != nil {
-		return nil, nil, err
+		return nil, err
 	}
 
 	certData, parsedCert, err := cert.LoadCert(
 		t.cfg.TLSCertPath, t.cfg.TLSKeyPath,
 	)
 	if err != nil {
-		return nil, nil, err
+		return nil, err
 	}
 
 	// Check to see if the certificate needs to be renewed. If it does, we
 	// return the newly generated certificate data instead.
 	reloadedCertData, err := t.maintainCert(parsedCert)
 	if err != nil {
-		return nil, nil, err
+		return nil, err
 	}
 	if reloadedCertData != nil {
 		certData = *reloadedCertData
 	}
 
 	tlsCfg := cert.TLSConfFromCert(certData)
-	cleanUp := t.setUpLetsEncrypt(&certData, tlsCfg)
 
-	return tlsCfg, cleanUp, nil
+	return tlsCfg, nil
 }
 
 // generateCertPair creates and writes a TLS pair to disk if the pair
@@ -474,7 +477,6 @@ func (t *TLSManager) SetCertificateBeforeUnlock() ([]grpc.ServerOption,
 	[]grpc.DialOption, func(net.Addr) (net.Listener, error), func(),
 	error) {
 
-	var cleanUp func()
 	if t.cfg.TLSEncryptKey {
 		_, err := t.loadEphemeralCertificate()
 		if err != nil {
@@ -482,15 +484,14 @@ func (t *TLSManager) SetCertificateBeforeUnlock() ([]grpc.ServerOption,
 				"ephemeral certificate: %v", err)
 		}
 	} else {
-		_, cleanUpFunc, err := t.generateOrRenewCert()
+		_, err := t.generateOrRenewCert()
 		if err != nil {
 			return nil, nil, nil, nil, fmt.Errorf("unable to "+
 				"generate or renew TLS certificate: %v", err)
 		}
-		cleanUp = cleanUpFunc
 	}
 
-	serverOpts, restDialOpts, restListen, err := t.getConfig()
+	serverOpts, restDialOpts, restListen, cleanUp, err := t.getConfig()
 	if err != nil {
 		return nil, nil, nil, nil, fmt.Errorf("unable to load TLS "+
 			"credentials: %v", err)


Test patch:
diff --git a/tls_manager_test.go b/tls_manager_test.go
index fcd2eadbfad..93583497158 100644
--- a/tls_manager_test.go
+++ b/tls_manager_test.go
@@ -53,9 +53,9 @@ func TestGenerateOrRenewCert(t *testing.T) {
 		TLSCertDuration: testTLSCertDuration,
 	}
 	tlsManager := NewTLSManager(cfg)
-	_, cleanUp, err := tlsManager.generateOrRenewCert()
+	_, err := tlsManager.generateOrRenewCert()
 	require.NoError(t, err)
-	_, _, _, err = tlsManager.getConfig()
+	_, _, _, cleanUp, err := tlsManager.getConfig()
 	require.NoError(t, err, "couldn't retrieve TLS config")
 	t.Cleanup(cleanUp)
 
@@ -86,7 +86,7 @@ func TestTLSManagerGenCert(t *testing.T) {
 	}
 	tlsManager := NewTLSManager(cfg)
 
-	_, _, err := tlsManager.generateOrRenewCert()
+	_, err := tlsManager.generateOrRenewCert()
 	require.NoError(t, err, "failed to generate new certificate")
 
 	// After this is run, a new certificate should be created and written