feat: update Dockerfiles and configuration for TLS support across multiple modules

Signed-off-by: d9pouces <github@19pouces.net>
This commit is contained in:
d9pouces
2026-09-05 22:23:34 +02:00
parent fc41dfaa07
commit a5a36fc7cd
20 changed files with 441 additions and 132 deletions
+5 -2
View File
@@ -2,13 +2,16 @@ uuid: 3d2f6a44-8b19-4c2b-9e7a-1f5c8a2d6b90
organisation: InterDiode
description: Apk test configuration
configurations:
certificates: []
certificates:
- name: Test CA
content: |
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
apt:
sources: []
keys: []
apk:
repositories:
- url: http://host.docker.internal:{{ 'APK_REPO_PORT' | env }}/
- url: https://host.docker.internal:{{ 'APK_REPO_PORT' | env }}/
key: |
{{ 'dockerfiles/repos/040d0af5f97a0a44.rsa.pub' | file | indent(8, first=true) }}
yum:
+5 -2
View File
@@ -2,7 +2,10 @@ uuid: 6a1d4e2c-9f3b-4a5d-8c7e-2b1f9a6d4c3e
organisation: InterDiode
description: Java test configuration
configurations:
certificates: []
certificates:
- name: Test CA
content: |
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
apt:
sources: []
keys: []
@@ -16,4 +19,4 @@ configurations:
java:
repositories:
- name: central
url: http://host.docker.internal:{{ 'JAVA_REPO_PORT' | env }}/
url: https://host.docker.internal:{{ 'JAVA_REPO_PORT' | env }}/
+5 -2
View File
@@ -2,7 +2,10 @@ uuid: 8f6a0e2a-5b7a-4d2c-9d1a-3c2b6b9c4b7e
organisation: InterDiode
description: Yum test configuration
configurations:
certificates: []
certificates:
- name: Test CA
content: |
{{ 'certs/test-ca.crt.pem' | file | indent(6, first=true) }}
apt:
sources: []
keys: []
@@ -11,7 +14,7 @@ configurations:
yum:
repositories:
- name: demo
index-url: http://host.docker.internal:{{ 'YUM_REPO_PORT' | env }}/
index-url: https://host.docker.internal:{{ 'YUM_REPO_PORT' | env }}/
keys: []
python:
repositories: []
+1 -1
View File
@@ -1,5 +1,5 @@
FROM alpine:3.20 AS detection
FROM interdiode/id_configurator:latest AS configuration
FROM interdiode/id-configurator:latest AS configuration
COPY --from=detection / /src/
COPY --from=generated-config Apk.yaml /config.yaml
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
+1 -2
View File
@@ -1,10 +1,9 @@
FROM debian:trixie-slim AS detection
FROM interdiode/id_configurator:latest AS configuration
FROM interdiode/id-configurator:latest AS configuration
COPY --from=detection / /src/
COPY --from=generated-config Apt.yaml /config.yaml
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
FROM debian:trixie-slim AS runtime
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates
COPY --from=configuration /dst/ /
RUN apt-get update \
&& apt-get install -y --no-install-recommends idconfiguratortestpkg \
+1 -1
View File
@@ -1,5 +1,5 @@
FROM maven:3.9-eclipse-temurin-21 AS detection
FROM interdiode/id_configurator:latest AS configuration
FROM interdiode/id-configurator:latest AS configuration
COPY --from=detection / /src/
COPY --from=generated-config Java.yaml /config.yaml
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
+1 -1
View File
@@ -1,5 +1,5 @@
FROM python:3.13-slim AS detection
FROM interdiode/id_configurator:latest AS configuration
FROM interdiode/id-configurator:latest AS configuration
COPY --from=detection / /src/
COPY --from=generated-config Python.yaml /config.yaml
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
+1 -1
View File
@@ -1,5 +1,5 @@
FROM rockylinux:9 AS detection
FROM interdiode/id_configurator:latest AS configuration
FROM interdiode/id-configurator:latest AS configuration
COPY --from=detection / /src/
COPY --from=generated-config Yum.yaml /config.yaml
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
+5 -2
View File
@@ -1,6 +1,7 @@
# Minimal Alpine (apk) repository with a signed index, serving one hand-built
# .apk package over plain HTTP, used by tox-docker to exercise the `apk`
# module integration test (tests/configs/Apk.yaml.tmpl,
# .apk package over TLS, using the same Test CA as the pypi-repo/apt-repo/
# yum-repo fixtures, used by tox-docker to exercise the `apk` module
# integration test (tests/configs/Apk.yaml.j2,
# tests/dockerfiles/Apk.Dockerfile). The package is signed with the static
# key pair 040d0af5f97a0a44.rsa / 040d0af5f97a0a44.rsa.pub, whose public
# half is what the `apk` module configuration is expected to install into
@@ -39,5 +40,7 @@ RUN arch_dir=$(ls /home/builder/packages/builder) \
FROM nginx:alpine
COPY --from=packagebuild /srv /srv
COPY repo-server.crt.pem /etc/nginx/certs/repo-server.crt.pem
COPY repo-server.key.pem /etc/nginx/certs/repo-server.key.pem
COPY apk-repo.nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8443
+4 -1
View File
@@ -1,7 +1,10 @@
server {
listen 8443 default_server;
listen 8443 ssl default_server;
server_name _;
ssl_certificate /etc/nginx/certs/repo-server.crt.pem;
ssl_certificate_key /etc/nginx/certs/repo-server.key.pem;
root /srv;
autoindex off;
}
+6 -3
View File
@@ -1,6 +1,7 @@
# Minimal Maven-compatible repository serving one hand-built jar over plain
# HTTP, used by tox-docker to exercise the `java` module integration test
# (tests/configs/Java.yaml.tmpl, tests/dockerfiles/Java.Dockerfile).
# Minimal Maven-compatible repository serving one hand-built jar over TLS,
# using the same Test CA as the pypi-repo/apt-repo/yum-repo/apk-repo
# fixtures, used by tox-docker to exercise the `java` module integration
# test (tests/configs/Java.yaml.j2, tests/dockerfiles/Java.Dockerfile).
#
# Built by tox-docker, whose build context is this directory (not the repo
# root) - every COPY source below must live under tests/dockerfiles/repos/.
@@ -10,5 +11,7 @@ RUN python3 /build_test_jar.py
FROM nginx:alpine
COPY --from=jarbuild /srv /srv
COPY repo-server.crt.pem /etc/nginx/certs/repo-server.crt.pem
COPY repo-server.key.pem /etc/nginx/certs/repo-server.key.pem
COPY java-repo.nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8443
+4 -1
View File
@@ -1,7 +1,10 @@
server {
listen 8443 default_server;
listen 8443 ssl default_server;
server_name _;
ssl_certificate /etc/nginx/certs/repo-server.crt.pem;
ssl_certificate_key /etc/nginx/certs/repo-server.key.pem;
root /srv;
autoindex off;
}
+6 -4
View File
@@ -1,7 +1,7 @@
# Minimal YUM/DNF repository serving one hand-built RPM package over plain
# HTTP (gpgcheck is off in the test config, so no TLS/signing is needed),
# used by tox-docker to exercise the `yum` module integration test
# (tests/configs/Yum.yaml.tmpl, tests/dockerfiles/Yum.Dockerfile).
# Minimal YUM/DNF repository serving one hand-built RPM package over TLS,
# using the same Test CA as the pypi-repo/apt-repo fixtures, used by
# tox-docker to exercise the `yum` module integration test
# (tests/configs/Yum.yaml.j2, tests/dockerfiles/Yum.Dockerfile).
#
# Built by tox-docker, whose build context is this directory (not the repo
# root) - every COPY source below must live under tests/dockerfiles/repos/.
@@ -16,5 +16,7 @@ RUN createrepo_c /srv
FROM nginx:alpine
COPY --from=packagebuild /srv /srv
COPY repo-server.crt.pem /etc/nginx/certs/repo-server.crt.pem
COPY repo-server.key.pem /etc/nginx/certs/repo-server.key.pem
COPY yum-repo.nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8443
+4 -1
View File
@@ -1,7 +1,10 @@
server {
listen 8443 default_server;
listen 8443 ssl default_server;
server_name _;
ssl_certificate /etc/nginx/certs/repo-server.crt.pem;
ssl_certificate_key /etc/nginx/certs/repo-server.key.pem;
root /srv;
autoindex off;
}
+180 -59
View File
@@ -1,11 +1,10 @@
import subprocess
from unittest.mock import MagicMock
import pytest
from id_configurator.config import Configuration
from id_configurator.context import Context
from id_configurator.modules.certificates import CertificatesModule
from id_configurator.modules.certificates import (
CertificatesModule,
_pem_name,
_resolve_target_dir,
)
CERT_CONTENT = "fake-cert-bytes"
@@ -32,38 +31,30 @@ def test_is_relevant_false_when_no_certificates(tmp_path) -> None:
assert CertificatesModule().is_relevant(context) is False
def _mock_success(monkeypatch, stderr: str = "") -> MagicMock:
mock_run = MagicMock(
return_value=subprocess.CompletedProcess(
args=[], returncode=0, stdout="", stderr=stderr
)
)
monkeypatch.setattr("id_configurator.modules.certificates.subprocess.run", mock_run)
return mock_run
def test_apply_writes_certificate_and_calls_update_ca_certificates(
tmp_path, monkeypatch
) -> None:
mock_run = _mock_success(monkeypatch)
def test_apply_writes_certificate_and_rebuilds_bundle(tmp_path) -> None:
dst = tmp_path / "dst"
context = Context(config=make_config(True), dst=dst, src=None)
CertificatesModule().apply(context)
written = dst / "usr/local/share/ca-certificates/new-root-ca.crt"
assert written.read_bytes() == b"fake-cert-bytes"
mock_run.assert_called_once()
args = mock_run.call_args.args[0]
assert args[0] == "update-ca-certificates"
assert "--certsconf" not in args
assert "--certsdir" not in args
local_cert = dst / "usr/local/share/ca-certificates/new-root-ca.crt"
assert local_cert.read_bytes() == b"fake-cert-bytes"
bundle = dst / "etc/ssl/certs/ca-certificates.crt"
assert bundle.read_bytes() == b"fake-cert-bytes\n"
pem = dst / "etc/ssl/certs/new-root-ca.pem"
assert pem.is_symlink()
assert (
pem.readlink().as_posix() == "/usr/local/share/ca-certificates/new-root-ca.crt"
)
openssl_default = dst / "usr/lib/ssl/cert.pem"
assert openssl_default.is_symlink()
assert openssl_default.readlink().as_posix() == "/etc/ssl/certs/ca-certificates.crt"
def test_apply_preserves_existing_local_certificates_from_src(
tmp_path, monkeypatch
) -> None:
mock_run = _mock_success(monkeypatch, stderr="some warning")
def test_apply_merges_conf_listed_and_local_certificates(tmp_path) -> None:
src = tmp_path / "src"
local_certs_dir = src / "usr/local/share/ca-certificates"
local_certs_dir.mkdir(parents=True)
@@ -71,9 +62,14 @@ def test_apply_preserves_existing_local_certificates_from_src(
(local_certs_dir / "new-root-ca.crt").write_bytes(
b"stale-bytes-should-be-overwritten"
)
(src / "etc/ca-certificates.conf").parent.mkdir(parents=True)
(src / "etc/ca-certificates.conf").write_text("existing.crt\n", encoding="utf-8")
(src / "usr/share/ca-certificates").mkdir(parents=True)
system_certs_dir = src / "usr/share/ca-certificates"
system_certs_dir.mkdir(parents=True)
# No trailing newline, to exercise the newline-normalization on append.
(system_certs_dir / "system-ca.crt").write_bytes(b"system-ca-bytes")
(src / "etc/ca-certificates.conf").parent.mkdir(parents=True, exist_ok=True)
(src / "etc/ca-certificates.conf").write_text(
"# a comment\n\n!disabled-ca.crt\nsystem-ca.crt\n", encoding="utf-8"
)
dst = tmp_path / "dst"
context = Context(config=make_config(True), dst=dst, src=src)
@@ -84,35 +80,160 @@ def test_apply_preserves_existing_local_certificates_from_src(
assert (local_dst / "existing.crt").read_bytes() == b"existing-bytes"
assert (local_dst / "new-root-ca.crt").read_bytes() == b"fake-cert-bytes"
args = mock_run.call_args.args[0]
assert "--certsconf" in args
assert str(src / "etc/ca-certificates.conf") in args
assert "--certsdir" in args
assert str(src / "usr/share/ca-certificates") in args
bundle = dst / "etc/ssl/certs/ca-certificates.crt"
# ca-certificates.conf entries first, then local certificates sorted by name.
assert bundle.read_bytes() == (
b"system-ca-bytes\nexisting-bytes\nfake-cert-bytes\n"
)
etc_certs = dst / "etc/ssl/certs"
assert (
etc_certs / "system-ca.pem"
).readlink().as_posix() == "/usr/share/ca-certificates/system-ca.crt"
assert (
etc_certs / "existing.pem"
).readlink().as_posix() == "/usr/local/share/ca-certificates/existing.crt"
assert (
etc_certs / "new-root-ca.pem"
).readlink().as_posix() == "/usr/local/share/ca-certificates/new-root-ca.crt"
# The disabled entry must not produce a symlink or appear in the bundle.
assert not (etc_certs / "disabled-ca.pem").exists()
def test_update_ca_certificates_raises_when_binary_missing(
tmp_path, monkeypatch
def test_apply_warns_and_skips_missing_conf_entry(tmp_path, caplog) -> None:
src = tmp_path / "src"
(src / "usr/share/ca-certificates").mkdir(parents=True)
(src / "etc/ca-certificates.conf").parent.mkdir(parents=True, exist_ok=True)
(src / "etc/ca-certificates.conf").write_text("missing.crt\n", encoding="utf-8")
dst = tmp_path / "dst"
context = Context(config=make_config(True), dst=dst, src=src)
with caplog.at_level("WARNING"):
CertificatesModule().apply(context)
assert "missing.crt" in caplog.text
bundle = dst / "etc/ssl/certs/ca-certificates.crt"
assert bundle.read_bytes() == b"fake-cert-bytes\n"
def test_pem_name_sanitizes_special_characters() -> None:
assert _pem_name("usr/share/ca-certificates/New Root, CA.crt") == "New_Root__CA.pem"
assert _pem_name("Vendor (Test).crt") == "Vendor_=Test=.pem"
def test_apply_skips_rhel_trust_store_when_absent(tmp_path) -> None:
dst = tmp_path / "dst"
context = Context(config=make_config(True), dst=dst, src=None)
CertificatesModule().apply(context)
assert not (dst / "etc/pki").exists()
def test_apply_updates_rhel_trust_store_when_present(tmp_path) -> None:
src = tmp_path / "src"
anchors_dir = src / "etc/pki/ca-trust/source/anchors"
anchors_dir.mkdir(parents=True)
(anchors_dir / "existing-anchor.pem").write_bytes(b"existing-anchor-bytes")
(anchors_dir / "new-root-ca.crt").write_bytes(b"stale-should-be-overwritten")
extracted_bundle = src / "etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"
extracted_bundle.parent.mkdir(parents=True)
# No trailing newline, to exercise the newline-normalization on append.
extracted_bundle.write_bytes(b"vendor-bundle-bytes")
dst = tmp_path / "dst"
context = Context(config=make_config(True), dst=dst, src=src)
CertificatesModule().apply(context)
anchors_dst = dst / "etc/pki/ca-trust/source/anchors"
assert (
anchors_dst / "existing-anchor.pem"
).read_bytes() == b"existing-anchor-bytes"
assert (anchors_dst / "new-root-ca.crt").read_bytes() == b"fake-cert-bytes"
bundle_dst = dst / "etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"
assert bundle_dst.read_bytes() == (
b"vendor-bundle-bytes\nexisting-anchor-bytes\nfake-cert-bytes\n"
)
def test_apply_rebuilds_rhel_trust_store_without_preexisting_bundle(tmp_path) -> None:
src = tmp_path / "src"
(src / "etc/pki/ca-trust/source/anchors").mkdir(parents=True)
dst = tmp_path / "dst"
context = Context(config=make_config(True), dst=dst, src=src)
CertificatesModule().apply(context)
bundle_dst = dst / "etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"
assert bundle_dst.read_bytes() == b"fake-cert-bytes\n"
def test_resolve_target_dir_returns_unchanged_when_not_a_symlink(tmp_path) -> None:
src = tmp_path / "src"
(src / "etc/ssl/certs").mkdir(parents=True)
context = Context(config=make_config(True), dst=tmp_path / "dst", src=src)
assert _resolve_target_dir(context, "etc/ssl/certs") == "etc/ssl/certs"
def test_resolve_target_dir_returns_unchanged_without_src(tmp_path) -> None:
context = Context(config=make_config(True), dst=tmp_path / "dst", src=None)
assert _resolve_target_dir(context, "etc/ssl/certs") == "etc/ssl/certs"
def test_resolve_target_dir_follows_absolute_symlink(tmp_path) -> None:
src = tmp_path / "src"
(src / "etc/pki/tls/certs").mkdir(parents=True)
(src / "etc/ssl").mkdir(parents=True)
(src / "etc/ssl/certs").symlink_to("/etc/pki/tls/certs")
context = Context(config=make_config(True), dst=tmp_path / "dst", src=src)
assert _resolve_target_dir(context, "etc/ssl/certs") == "etc/pki/tls/certs"
def test_resolve_target_dir_follows_relative_symlink(tmp_path) -> None:
src = tmp_path / "src"
(src / "etc/pki/tls/certs").mkdir(parents=True)
(src / "etc/ssl").mkdir(parents=True)
(src / "etc/ssl/certs").symlink_to("../pki/tls/certs")
context = Context(config=make_config(True), dst=tmp_path / "dst", src=src)
assert _resolve_target_dir(context, "etc/ssl/certs") == "etc/pki/tls/certs"
def test_resolve_target_dir_falls_back_when_relative_symlink_escapes_src(
tmp_path,
) -> None:
monkeypatch.setattr(
"id_configurator.modules.certificates.subprocess.run",
MagicMock(side_effect=FileNotFoundError()),
)
context = Context(config=make_config(True), dst=tmp_path / "dst", src=None)
src = tmp_path / "src"
(src / "etc/ssl").mkdir(parents=True)
# Escapes past <src> itself via enough ".." components.
(src / "etc/ssl/certs").symlink_to("../../../../../outside")
context = Context(config=make_config(True), dst=tmp_path / "dst", src=src)
with pytest.raises(RuntimeError, match="update-ca-certificates is not available"):
CertificatesModule().apply(context)
assert _resolve_target_dir(context, "etc/ssl/certs") == "etc/ssl/certs"
def test_update_ca_certificates_raises_on_failure(tmp_path, monkeypatch) -> None:
error = subprocess.CalledProcessError(
returncode=1, cmd=["update-ca-certificates"], stderr="boom"
)
monkeypatch.setattr(
"id_configurator.modules.certificates.subprocess.run",
MagicMock(side_effect=error),
)
context = Context(config=make_config(True), dst=tmp_path / "dst", src=None)
def test_apply_writes_bundle_to_rhel_style_symlinked_etccertsdir(tmp_path) -> None:
src = tmp_path / "src"
(src / "etc/pki/tls/certs").mkdir(parents=True)
(src / "etc/ssl").mkdir(parents=True)
(src / "etc/ssl/certs").symlink_to("/etc/pki/tls/certs")
with pytest.raises(RuntimeError, match="update-ca-certificates failed"):
CertificatesModule().apply(context)
dst = tmp_path / "dst"
context = Context(config=make_config(True), dst=dst, src=src)
CertificatesModule().apply(context)
assert not (dst / "etc/ssl/certs").exists()
bundle = dst / "etc/pki/tls/certs/ca-certificates.crt"
assert bundle.read_bytes() == b"fake-cert-bytes\n"
pem = dst / "etc/pki/tls/certs/new-root-ca.pem"
assert pem.is_symlink()
# /usr/lib/ssl/cert.pem is Debian/Ubuntu-specific: not OpenSSL's default
# CAfile on this (RHEL-style) system, so it must not be created.
assert not (dst / "usr/lib/ssl/cert.pem").exists()
+2 -10
View File
@@ -1,5 +1,4 @@
import json
import subprocess
from unittest.mock import MagicMock
from typer.testing import CliRunner
@@ -66,14 +65,7 @@ def test_apply_skips_irrelevant_modules(tmp_path, caplog) -> None:
assert "skipping module python" in caplog.text
def test_apply_runs_relevant_module(tmp_path, monkeypatch) -> None:
mock_run = MagicMock(
return_value=subprocess.CompletedProcess(
args=[], returncode=0, stdout="", stderr=""
)
)
monkeypatch.setattr("id_configurator.modules.certificates.subprocess.run", mock_run)
def test_apply_runs_relevant_module(tmp_path) -> None:
config_path = write_config(tmp_path, CONFIG_WITH_CERT)
dst = tmp_path / "dst"
result = runner.invoke(
@@ -82,7 +74,7 @@ def test_apply_runs_relevant_module(tmp_path, monkeypatch) -> None:
assert result.exit_code == 0
assert (dst / "usr/local/share/ca-certificates/my-ca.crt").read_bytes() == b"fake"
mock_run.assert_called_once()
assert (dst / "etc/ssl/certs/ca-certificates.crt").read_bytes() == b"fake\n"
def test_main_default_name_calls_app(monkeypatch) -> None:
+4 -1
View File
@@ -122,8 +122,11 @@ def test_apply_adds_certificate_to_pkcs12_and_jks_keystores(tmp_path) -> None:
JavaModule().apply(context)
# The written truststore's trust-only entries are unencrypted (matching
# the JDK's own cacerts), so they load with no password at all - the
# password a caller would use for a source-of-truth JDK-style store.
pkcs12_result = pkcs12.load_pkcs12(
(dst / "usr/lib/jvm/java-21/lib/security/cacerts").read_bytes(), b"changeit"
(dst / "usr/lib/jvm/java-21/lib/security/cacerts").read_bytes(), None
)
pkcs12_aliases = {entry.friendly_name for entry in pkcs12_result.additional_certs}
assert pkcs12_aliases == {b"already-trusted-ca", b"interdiode-root-ca"}