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:" +
"DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384\";\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 org.slf4j.LoggerFactory
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.Path
import java.nio.file.Paths
import java.time.Duration
/**
* Manages the opt-in nginx+certbot Docker container that serves GitTally over
@@ -36,9 +31,6 @@ class NginxProxyManager(
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. */
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)
val dhParams = settings.certbotConf.resolve("ssl-dhparams.pem")
if (!Files.exists(dhParams)) {
dhParamsDownloader(dhParams)
writeBundledDhParams(dhParams)
}
}
@@ -424,19 +416,16 @@ class NginxProxyManager(
fun defaultContainerName(repoDir: Path): String =
"gittally-nginx-" + repoDir.fileName.toString().replace(Regex("[^A-Za-z0-9_.-]"), "-")
private fun downloadDhParams(target: Path) {
val response =
HttpClient
.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.followRedirects(HttpClient.Redirect.NORMAL)
.build()
.send(
HttpRequest.newBuilder(URI.create(NginxConfigFiles.DH_PARAMS_URL)).GET().build(),
HttpResponse.BodyHandlers.ofString(),
)
check(response.statusCode() == 200) { "downloading ssl-dhparams.pem failed with HTTP ${response.statusCode()}" }
Files.writeString(target, response.body())
/**
* Certbot's pinned DH parameters (RFC 7919 ffdhe2048), bundled as a resource:
* certbot removed the file from its repository, so it can no longer be downloaded.
*/
private fun writeBundledDhParams(target: Path) {
val resource =
checkNotNull(NginxProxyManager::class.java.getResourceAsStream("/nginx/ssl-dhparams.pem")) {
"bundled nginx/ssl-dhparams.pem resource is missing"
}
resource.use { Files.copy(it, target) }
}
}
}
@@ -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.workingDir = repoDir
manager.dhParamsDownloader = { Files.writeString(it, "dh-params") }
manager.sleeper = { sleepCount++ }
}
@@ -198,7 +197,8 @@ class NginxProxyManagerTest : FunSpec() {
"--register-unsafely-without-email",
)
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") {