Bundle certbot's DH parameters as a resource instead of downloading them

certbot removed ssl-dhparams.pem from its repository, so the managed-nginx
startup failed with HTTP 404 on fresh installations (the legacy script only
kept working through its year-old state-dir cache). The RFC 7919 ffdhe2048
parameters are now a classpath resource; the download seam and
NginxConfigFiles.DH_PARAMS_URL are gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-10 15:34:20 +02:00
co-authored by Claude Fable 5
parent 66183f9765
commit 800fcae2f7
4 changed files with 21 additions and 27 deletions
@@ -77,7 +77,4 @@ object NginxConfigFiles {
"ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" + "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" +
"DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384\";\n" + "DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384\";\n" +
"ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;\n" "ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;\n"
/** Certbot's pinned DH parameters, downloaded once into the state dir (like legacy). */
const val DH_PARAMS_URL: String = "https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem"
} }
@@ -6,14 +6,9 @@ import de.hoennig.gittally.config.ConfigLoader
import de.hoennig.gittally.git.GitCommandRunner import de.hoennig.gittally.git.GitCommandRunner
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import java.time.Duration
/** /**
* Manages the opt-in nginx+certbot Docker container that serves GitTally over * Manages the opt-in nginx+certbot Docker container that serves GitTally over
@@ -36,9 +31,6 @@ class NginxProxyManager(
var workingDir: Path = Paths.get(".") var workingDir: Path = Paths.get(".")
/** Replaceable for tests: fetches certbot's pinned DH parameters into [target]; throws on failure. */
internal var dhParamsDownloader: (target: Path) -> Unit = ::downloadDhParams
/** Replaceable for tests: the wait between port-conflict re-checks. */ /** Replaceable for tests: the wait between port-conflict re-checks. */
internal var sleeper: (millis: Long) -> Unit = Thread::sleep internal var sleeper: (millis: Long) -> Unit = Thread::sleep
@@ -188,7 +180,7 @@ class NginxProxyManager(
Files.writeString(settings.certbotConf.resolve("options-ssl-nginx.conf"), NginxConfigFiles.SSL_OPTIONS) Files.writeString(settings.certbotConf.resolve("options-ssl-nginx.conf"), NginxConfigFiles.SSL_OPTIONS)
val dhParams = settings.certbotConf.resolve("ssl-dhparams.pem") val dhParams = settings.certbotConf.resolve("ssl-dhparams.pem")
if (!Files.exists(dhParams)) { if (!Files.exists(dhParams)) {
dhParamsDownloader(dhParams) writeBundledDhParams(dhParams)
} }
} }
@@ -424,19 +416,16 @@ class NginxProxyManager(
fun defaultContainerName(repoDir: Path): String = fun defaultContainerName(repoDir: Path): String =
"gittally-nginx-" + repoDir.fileName.toString().replace(Regex("[^A-Za-z0-9_.-]"), "-") "gittally-nginx-" + repoDir.fileName.toString().replace(Regex("[^A-Za-z0-9_.-]"), "-")
private fun downloadDhParams(target: Path) { /**
val response = * Certbot's pinned DH parameters (RFC 7919 ffdhe2048), bundled as a resource:
HttpClient * certbot removed the file from its repository, so it can no longer be downloaded.
.newBuilder() */
.connectTimeout(Duration.ofSeconds(10)) private fun writeBundledDhParams(target: Path) {
.followRedirects(HttpClient.Redirect.NORMAL) val resource =
.build() checkNotNull(NginxProxyManager::class.java.getResourceAsStream("/nginx/ssl-dhparams.pem")) {
.send( "bundled nginx/ssl-dhparams.pem resource is missing"
HttpRequest.newBuilder(URI.create(NginxConfigFiles.DH_PARAMS_URL)).GET().build(), }
HttpResponse.BodyHandlers.ofString(), resource.use { Files.copy(it, target) }
)
check(response.statusCode() == 200) { "downloading ssl-dhparams.pem failed with HTTP ${response.statusCode()}" }
Files.writeString(target, response.body())
} }
} }
} }
@@ -0,0 +1,8 @@
-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg==
-----END DH PARAMETERS-----
@@ -126,7 +126,6 @@ class NginxProxyManagerTest : FunSpec() {
} }
manager = NginxProxyManager(commandRunner, configLoader) manager = NginxProxyManager(commandRunner, configLoader)
manager.workingDir = repoDir manager.workingDir = repoDir
manager.dhParamsDownloader = { Files.writeString(it, "dh-params") }
manager.sleeper = { sleepCount++ } manager.sleeper = { sleepCount++ }
} }
@@ -198,7 +197,8 @@ class NginxProxyManagerTest : FunSpec() {
"--register-unsafely-without-email", "--register-unsafely-without-email",
) )
Files.readString(stateDir.resolve("certbot/conf/options-ssl-nginx.conf")) shouldBe NginxConfigFiles.SSL_OPTIONS Files.readString(stateDir.resolve("certbot/conf/options-ssl-nginx.conf")) shouldBe NginxConfigFiles.SSL_OPTIONS
Files.readString(stateDir.resolve("certbot/conf/ssl-dhparams.pem")) shouldBe "dh-params" // the bundled RFC 7919 ffdhe2048 parameters, vendored because certbot removed the download
Files.readString(stateDir.resolve("certbot/conf/ssl-dhparams.pem")) shouldContain "BEGIN DH PARAMETERS"
} }
test("start with an existing certificate uses the full config immediately and renews") { test("start with an existing certificate uses the full config immediately and renews") {