Software Supply Chain Security: SBOM, Signierung und Blockade
1. Übersicht
Eine typische Spring-Boot-Anwendung besteht aus wenigen tausend Zeilen eigenem Code. Der Rest stammt aus hunderten Third-Party-Bibliotheken, dem Build-Tool, dem CI-Runner und dem Container-Base-Image. Jede dieser Komponenten kann angegriffen werden. Das ist das Problem der Software Supply Chain Security.
Dieser Artikel behandelt drei praktische Säulen: SBOM erzeugen, um zu wissen, was Sie verwenden, Artifacts signieren, um nachzuweisen, dass sie nicht manipuliert wurden, und böse Pakete blockieren, bereits bei der Dependency-Auflösung. Nach der Lektüre können Sie diese Schritte in ein bestehendes Maven-Projekt und eine CI-Pipeline integrieren.
2. Warum Supply Chain Security wichtig ist
Angreifer haben erkannt, dass es viel effizienter ist, eine weit verbreitete Bibliothek anzugreifen, als einzelne Anwendungen zu kompromittieren. Indem sie bösartigen Code in ein Paket einfügen, können sie alle Projekte erreichen, die dieses Paket verwenden.
Häufige Angriffsarten:
- Typosquatting: Ein Paket mit einem Namen veröffentlichen, der einem echten Paket ähnelt, z. B. mit einem Tippfehler, und darauf warten, dass Entwickler sich vertippen.
- Dependency Confusion: Ein Paket mit dem gleichen Namen wie ein internes Paket in der öffentlichen Registry veröffentlichen, aber mit einer höheren Version. Ein falsch konfiguriertes Build-Tool wählt dann die öffentliche Version.
- Übernahme von Maintainer-Konten: Die Anmeldedaten eines Bibliothekswartungsprogramms werden kompromittiert und ein „legales” Release mit bösartigem Code wird veröffentlicht.
- Build-System-Angriff: Der Quellcode ist sauber, aber der CI-Runner oder das Build-Skript wird manipuliert, sodass das resultierende Artifact kompromittiert ist.
- Sicherheitslücken in transitiven Abhängigkeiten: Sie deklarieren die Bibliothek nicht direkt, aber sie befindet sich dennoch im Classpath.
Parallel dazu beginnen viele Organisationen und geschäftliche Kunden ihre Softwareanbieter aufzufordern, SBOMs einzureichen und den Build-Ursprung nachzuweisen. Supply Chain Security ist nicht mehr nur „Aufgabe des Security-Teams”, sondern Anforderung im Alltag.
3. SBOM: Wissen, was Sie verwenden
SBOM (Software Bill of Materials) ist eine vollständige Liste aller Komponenten einer Software, einschließlich Name, Version, Lizenz, Hash und Abhängigkeitsbeziehungen. Stellen Sie es sich wie die Zutatenliste auf einer Lebensmittelverpackung vor.
Die zwei beliebtesten Formate sind CycloneDX und SPDX. Das Java-Ökosystem unterstützt CycloneDX gut, daher verwenden wir dieses Format hier.
3.1. SBOM mit Maven generieren
Fügen Sie das CycloneDX-Plugin zu pom.xml hinzu:
<properties>
<!-- Ersetzen Sie dies durch die neueste Version, die Sie auf Maven Central überprüft haben -->
<cyclonedx.version>REPLACE_WITH_LATEST</cyclonedx.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>${cyclonedx.version}</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>makeAggregateBom</goal>
</goals>
</execution>
</executions>
<configuration>
<outputFormat>json</outputFormat>
<includeTestScope>false</includeTestScope>
</configuration>
</plugin>
</plugins>
</build>
Erklärung:
makeAggregateBommergt Abhängigkeiten aller Module in einem Multi-Module-Projekt in eine Datei.- An die
package-Phase binden, damit SBOM immer zusammen mit dem JAR generiert wird. Niemand muss das manuell ausführen. includeTestScope=falseschließt JUnit, Mockito usw. aus dem SBOM aus, da sie nicht zur Laufzeit vorhanden sind.
Build ausführen:
mvn clean package
Das Plugin erstellt die Datei target/bom.json. Ein Teil des Inhalts sieht folgendermaßen aus:
{
"bomFormat": "CycloneDX",
"components": [
{
"group": "com.fasterxml.jackson.core",
"name": "jackson-databind",
"version": "...",
"purl": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@...",
"hashes": [ { "alg": "SHA-256", "content": "..." } ],
"licenses": [ ... ]
},
...
]
}
Das wichtigste Feld ist purl (Package URL). Dies ist der Standard-Identifier, den Vulnerability-Scanning-Tools verwenden, um sich mit der CVE-Datenbank abzugleichen.
3.2. Wofür wird SBOM verwendet
SBOM an sich schützt nicht vor viel. Sein Wert liegt darin, wie Sie es nutzen:
- Vulnerability-Scanning:
bom.jsonin ein SBOM-Scanning-Tool in Ihrer CI eingeben. Wenn es neue CVEs gibt, können Sie ältere SBOMs neu scannen, ohne neu zu bauen. - Die Frage „sind wir betroffen?” beantworten: Wenn eine kritische Sicherheitslücke auftritt, suchen Sie nach
purlin allen Service-SBOMs, statt jedes Team einzeln zu fragen. - Lizenz-Überprüfung: Ermitteln Sie, welche Bibliotheken Lizenzen haben, die nicht mit der Unternehmensrichtlinie konform sind.
Spring Boot 3.x in neueren Versionen hat auch einen Actuator-Endpoint sbom. Wenn die SBOM-Datei in die Anwendung gepackt wird (mit spring-boot-starter-parent und dem CycloneDX-Plugin), können Sie diesen Endpoint aktivieren:
management.endpoints.web.exposure.include=health,sbom
Rufen Sie GET /actuator/sbom auf, um die SBOM der laufenden Instanz zu sehen. Dies ist hilfreich, wenn Sie genau wissen müssen, welche Version auf Production läuft.
Hinweis: SBOM listet alle Bibliotheken und Versionen auf, die ein Angreifer nutzen kann, um nach Sicherheitslücken zu suchen. Machen Sie den
sbom-Endpoint nicht öffentlich. Begrenzen Sie ihn auf Ihr internes Netzwerk oder schützen Sie ihn mit Authentifizierung.
4. Artifact-Signierung: Integrität und Herkunftsnachweis
SBOM sagt Ihnen, was Artifact enthält. Eine digitale Signatur sagt Ihnen, woher das Artifact stammt und dass es nicht verändert worden ist, seit es signiert wurde.
4.1. Checksum: notwendig, aber nicht ausreichend
Die einfachste Methode ist die Hash-Überprüfung. Die folgende Klasse berechnet SHA-256 einer Datei und vergleicht es mit einem erwarteten Wert:
package dev.nonla.supplychain;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
public final class ArtifactVerifier {
private ArtifactVerifier() {
}
public static String sha256(Path file) throws IOException {
MessageDigest digest = newDigest();
try (InputStream in = Files.newInputStream(file)) {
byte[] buffer = new byte[8192];
int read;
// Datei in 8-KB-Blöcken lesen, um bei großen Artifacts den Speicher nicht zu überlasten
while ((read = in.read(buffer)) != -1) {
digest.update(buffer, 0, read);
}
}
// HexFormat (seit Java 17) konvertiert Bytes in Hex-String ohne externe Biblioteken
return HexFormat.of().formatHex(digest.digest());
}
public static void verify(Path file, String expectedSha256) throws IOException {
String actual = sha256(file);
// MessageDigest.isEqual vergleicht in konstanter Zeit - gute Gewohnheit für Sicherheitsvergleiche
boolean matches = MessageDigest.isEqual(
actual.getBytes(StandardCharsets.US_ASCII),
expectedSha256.toLowerCase().getBytes(StandardCharsets.US_ASCII));
if (!matches) {
throw new SecurityException(
"Checksum mismatch for " + file.getFileName() + ": " + actual);
}
}
private static MessageDigest newDigest() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 not available", e);
}
}
}
Beachtenswert:
- Die Datei wird in 8-KB-Blöcken gelesen, sodass auch große Artifacts den Speicher nicht überlasten.
HexFormat(seit Java 17) konvertiert Bytes in Hex-Strings ohne externe Bibliotheken.MessageDigest.isEqualvergleicht in konstanter Zeit. Hier ist dies nicht kritisch, aber es ist eine gute Gewohnheit, bei sicherheitsrelevanten Vergleichen konstante Zeit zu verwenden.
Test für das Hauptverhalten mit JUnit 5 und AssertJ:
package dev.nonla.supplychain;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class ArtifactVerifierTest {
private static final String HELLO_SHA256 =
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
@TempDir
Path tempDir;
@Test
void givenUntouchedArtifact_whenVerify_thenPasses() throws Exception {
Path jar = Files.writeString(tempDir.resolve("order-service.jar"), "hello");
assertThat(ArtifactVerifier.sha256(jar)).isEqualTo(HELLO_SHA256);
assertThatCode(() -> ArtifactVerifier.verify(jar, HELLO_SHA256))
.doesNotThrowAnyException();
}
@Test
void givenTamperedArtifact_whenVerify_thenThrows() throws Exception {
Path jar = Files.writeString(tempDir.resolve("order-service.jar"), "hello!");
assertThatThrownBy(() -> ArtifactVerifier.verify(jar, HELLO_SHA256))
.isInstanceOf(SecurityException.class)
.hasMessageContaining("order-service.jar");
}
}
Nur das Hinzufügen eines !-Zeichens führt zu einem völlig anderen Hash, und verify wirft eine SecurityException.
Das Problem ist: Wenn ein Angreifer das Artifact ersetzen kann, kann er normalerweise auch die daneben liegende .sha256-Datei ersetzen. Checksums stellen nur Integrität sicher, nicht die Herkunft. Um die Herkunft zu beweisen, benötigen Sie eine digitale Signatur.
4.2. Signieren mit Sigstore cosign
Die traditionelle Methode ist die GPG-Signierung. Das funktioniert, aber die Verwaltung von Langzeit-Keys ist mühsam: Sie müssen sie speichern, rotieren und widerrufen, wenn sie kompromittiert werden. Sigstore bietet einen anderen Ansatz namens keyless signing. Beim Signieren nimmt cosign eine OIDC-Identität (z. B. die Identität des CI-Workflows), fordert ein kurzfristiges Zertifikat an, signiert und protokolliert es in einem öffentlichen Transparency Log.
Eine JAR-Datei in der CI signieren:
cosign sign-blob target/order-service.jar \
--bundle order-service.jar.sigstore.json \
--yes
Die .sigstore.json-Datei enthält die Signatur, das Zertifikat und den Nachweis, der in das Transparency Log geschrieben wurde. Die empfangende Seite verifiziert wie folgt:
cosign verify-blob target/order-service.jar \
--bundle order-service.jar.sigstore.json \
--certificate-identity-regexp "^https://github.com/acme/order-service/" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
Wenn die Verifizierung erfolgreich ist:
Verified OK
Wichtig sind die Parameter --certificate-identity* und --certificate-oidc-issuer. Ohne sie wissen Sie nur, dass „jemand signiert hat”, nicht ob der Unterzeichner Ihre Pipeline ist. Für Container-Images ist der Prozess ähnlich, wobei cosign sign und cosign verify auf den Image-Digest angewendet werden.
4.3. In die Pipeline integrieren
Ein minimales GitHub-Actions-Job, das JAR und SBOM signiert:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # erforderlich für cosign, um OIDC-Token zu erhalten
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- uses: sigstore/cosign-installer@v3
- run: mvn -B clean package
- run: |
cosign sign-blob target/order-service.jar --bundle order-service.jar.sigstore.json --yes
cosign sign-blob target/bom.json --bundle bom.json.sigstore.json --yes
Die Berechtigung id-token: write erlaubt dem Workflow, ein OIDC-Token zu erhalten. Sie speichern keine private Keys in Secrets. Die Signierung von bom.json zeigt dem Empfänger, dass die SBOM wirklich mit diesem Artifact geliefert wird, nicht von woanders.
5. Böse Pakete von Anfang an blockieren
Die Signierung von Artifacts schützt Ihre Ausgaben. Dieser Abschnitt schützt die Eingaben, nämlich die Abhängigkeiten, die Sie abrufen.
5.1. Nur über ein kontrolliertes Repository auflösen
Konfigurieren Sie ~/.m2/settings.xml (oder Settings in CI), damit alle Anfragen über ein internes Repository gehen und Fehlschlag auftreten, wenn die Checksumme nicht übereinstimmt:
<settings>
<mirrors>
<mirror>
<id>internal</id>
<mirrorOf>*</mirrorOf>
<url>https://repo.acme.internal/maven-all/</url>
</mirror>
</mirrors>
<profiles>
<profile>
<id>strict</id>
<repositories>
<repository>
<id>internal</id>
<url>https://repo.acme.internal/maven-all/</url>
<releases>
<!-- checksumPolicy=fail bewirkt, dass der Build stoppt, anstatt nur zu warnen, wenn die Checksumme falsch ist. -->
<checksumPolicy>fail</checksumPolicy>
</releases>
<snapshots>
<!-- Snapshots von externen Quellen deaktivieren, da Snapshots den Inhalt ändern können, ohne die Version zu ändern. -->
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>
<activeProfiles>
<activeProfile>strict</activeProfile>
</activeProfiles>
</settings>
mirrorOf=*fängt alle Repositories ab, einschließlich derer, diepom.xmleines Drittanbieters deklariert hat, müssen durch den internen Proxy gehen.checksumPolicy=failführt dazu, dass der Build stoppt, anstatt nur zu warnen, wenn die Checksumme falsch ist.- Deaktivieren Sie Snapshots von externen Quellen, da Snapshots den Inhalt ändern können, ohne die Version zu ändern.
Um Dependency Confusion zu bekämpfen, konfigurieren Sie den Repository-Proxy so, dass interne groupIds (z. B. com.acme.*) nur von dem internen Repository bezogen werden, nie von öffentlichen Quellen. Maven Central erfordert eine Namespace-Bestätigung beim Veröffentlichen, daher ist das Risiko niedriger als bei anderen Registries, aber Sie sollten sich nicht vollständig darauf verlassen.
5.2. Richtlinien mit Maven Enforcer erzwingen
Das Enforcer-Plugin wandelt Richtlinien in Build-Fehler um:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>${enforcer.version}</version>
<executions>
<execution>
<id>supply-chain-rules</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<bannedDependencies>
<excludes>
<exclude>log4j:log4j</exclude>
<exclude>commons-collections:commons-collections:[,3.2.2)</exclude>
</excludes>
</bannedDependencies>
<requireUpperBoundDeps/>
<requireReleaseDeps>
<onlyWhenRelease>false</onlyWhenRelease>
</requireReleaseDeps>
</rules>
</configuration>
</execution>
</executions>
</plugin>
bannedDependenciesblockiert Bibliotheken, die nicht mehr unterstützt werden, oder Versionsbereiche mit bekannten Sicherheitslücken. Diese Regel gilt auch für transitive Abhängigkeiten.requireUpperBoundDepsmeldet einen Fehler, wenn Maven stillschweigend eine niedrigere Version wählt als von einer anderen Bibliothek angefordert.requireReleaseDepsverbietet SNAPSHOT-Versionen in Abhängigkeiten.
Bei Verstoß stoppt der Build mit einer Meldung wie:
[ERROR] Rule 0: org.apache.maven.enforcer.rules.dependency.BannedDependencies failed with message:
com.acme:order-service:jar:1.0.0
log4j:log4j:jar:1.2.17 <--- banned via the exclude/include list
5.3. Kleine Gewohnheiten, große Auswirkungen
- Versionen genau festlegen. Verwenden Sie keine Versionsbereiche wie
[1.0,)oderLATEST, da Builds heute und morgen unterschiedlich sein können. - Bei neuen Abhängigkeiten überprüfen: Verifizieren Sie den exakten Namen, groupId, Benutzer-Count und Release-Historie. Eine unbekannte Bibliothek mit ähnlichem Namen wie eine bekannte Bibliothek ist ein Zeichen für Typosquatting.
- Aktualisierungen automatisieren mit einem Bot, der Pull Requests erstellt, aber CI-Tests und SBOM-Scans vor dem Merge ausführen lassen.
- Abhängigkeiten reduzieren: Was nicht im Classpath ist, kann nicht angegriffen werden.
6. Häufige Fehler und wann Sie es nicht übertreiben sollten
Hinweis: Ein SBOM zu generieren, das niemand liest, ist fast nutzlos. Verbinden Sie SBOM mit einem automatisierten Scan-Schritt und designieren Sie eine Person oder ein Team, die für die Behebung der Ergebnisse verantwortlich ist.
Einige häufige Fehler:
- Artifacts signieren, aber niemand verifiziert. Signaturen haben nur Wert, wenn der Deployment-Schritt (oder ein Kubernetes-Admission-Controller) Artifacts ohne gültige Signatur ablehnt.
- Signaturen verifizieren, ohne die Identität zu überprüfen. Wie in Abschnitt 4.2 erwähnt, ist „signiert” anders als „signiert von meiner Pipeline”.
- SBOM aus Quellcode statt aus dem endgültigen Artifact generieren. Wenn der Packaging-Schritt Bibliotheken hinzufügt oder entfernt, wird das SBOM ungenau. Generieren Sie SBOM im gleichen Build-Schritt, der das Deployment-Artifact erzeugt.
- Sperrlisten sind zu lang und nicht aktuell. Dann werden Teams versuchen, den Enforcer zu umgehen, und die Richtlinie wird wirkungslos.
Wann Sie nicht stark investieren sollten:
- Prototypen, interne Skripte, die einmalig laufen, Lernprojekte. Versions-Pinning und ein Repository-Mirror sind ausreichend. Keyless Signing und Admission Policies erfordern mehr Aufwand als den Nutzen bringen.
- Team hat noch keinen Vulnerability-Handling-Prozess. Hunderte CVE-Warnungen auf einmal ohne Triage führen zu „Alert Fatigue”. Beginnen Sie mit SBOM und Scanning auf hohem Schweregrad, dann schrittweise erweitern.
- Völlig abgeschottete Umgebung (Air-Gapped). Keyless Signing hängt von externen OIDC-Services und Transparency Logs ab. In dieser Umgebung ist das Signieren mit selbstverwalteten Keys (GPG oder cosign mit Key Pair) besser geeignet.
7. Fazit
Software Supply Chain Security dreht sich um drei Fragen: Was verwende ich, ist dieses Artifact wirklich von mir, und kann ich den Abhängigkeiten trauen, die ich nutze? SBOM beantwortet die erste Frage, digitale Signaturen mit Sigstore die zweite, und ein kontrolliertes Repository mit Maven Enforcer hilft bei der dritten.
Sie müssen nicht alles an einem Tag einführen. Ein sinnvoller Fahrplan ist: Versionen pinnen und ein internes Repository-Mirror nutzen, dann SBOM in jedem Build generieren, anschließend Enforcer hinzufügen, und schließlich Artifacts signieren mit obligatorischer Verifizierung beim Deployment. Jeder Schritt reduziert das Risiko deutlich und kann in Ihrer bestehenden Pipeline automatisiert werden.