Securing Repositories
Every JAR, POM, and plugin your project uses is fetched from a repository, and every repository you declare is an implicit statement of trust. If an attacker can influence what a repository serves, or trick your build into asking a repository it shouldn’t, they can inject arbitrary code into your build.
This page covers two practices that protect the repository layer:
-
Use trusted repositories: declare only repositories you control or explicitly vet, over HTTPS, in one central place.
-
Filter repository content: restrict which coordinates each repository is allowed to serve, and route sensitive artifacts to exclusive sources.
Use Trusted Repositories
A trusted repository is one whose contents you can vouch for. That trust usually comes from one of three places:
-
You run the repository yourself (an internal Artifactory, Nexus, or similar).
-
The repository is operated by an organization with a strong integrity record (for example, Maven Central).
-
The repository serves artifacts you have already verified out-of-band, typically through checksums or signatures recorded in your build.
Every other repository is untrusted by default, even if it appears to work. An untrusted repository can serve a legitimate artifact today and a compromised one tomorrow; your build has no way to tell the difference unless verification is in place.
Centralize Repository Declarations
Repositories declared inside individual subprojects are hard to audit.
A reviewer looking at settings.gradle.kts cannot tell whether a subproject three levels deep has silently added a repository that the rest of the build knows nothing about.
Since Gradle 6.8, dependencyResolutionManagement in settings.gradle(.kts) provides a central place to declare project repositories (see Centralizing Repository Declarations for the full reference):
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
repositoriesMode has three values:
| Mode | Behavior |
|---|---|
|
Any repository declared in a subproject’s |
|
Repositories declared in subprojects are ignored, and Gradle logs a warning for each. The settings repositories always win. |
|
Any repository declaration in a subproject causes the build to fail. Nothing outside |
For security-sensitive builds, use FAIL_ON_PROJECT_REPOS.
It guarantees that the only repositories your build will ever contact are the ones listed in settings.gradle(.kts), where they are visible to every reviewer and every audit.
The default is PREFER_PROJECT. Setting dependencyResolutionManagement.repositories { … } without also setting repositoriesMode still lets subprojects override the central list. Set the mode explicitly.
|
Plugin repositories follow a separate rule.
Declare them once in settings.gradle(.kts) under pluginManagement:
pluginManagement {
repositories {
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
}
}
pluginManagement {
repositories {
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
pluginManagement does not have a repositoriesMode equivalent. Plugin repositories are always resolved from settings.gradle(.kts), so the concern that motivates FAIL_ON_PROJECT_REPOS does not apply, no subproject can add a plugin repository.
|
Prefer an Internal Mirror
Consuming public repositories directly means your build depends on the availability and integrity of an upstream service you do not control. An internal mirror sits between your build and the public internet and gives you three properties that a direct dependency on Maven Central cannot:
-
Auditability: every artifact your build has ever downloaded is recorded in one place you own.
-
Reproducibility: artifacts remain available even if they are yanked or deleted upstream.
-
Containment: you can block or quarantine a compromised artifact for every project at once, without touching every build file in the organization.
A mirror doesn’t remove trust from the equation. It relocates it. Whatever you used to trust in the upstream now lives in the mirror, and any weakness in how you run the mirror becomes a weakness in every build that depends on it. Before adopting one:
-
Serve the mirror over HTTPS. An HTTP-only mirror is a downgrade, not an upgrade, over talking to Maven Central directly.
-
Verify artifacts on ingest. When the mirror pulls an artifact from an upstream repository for the first time, check the artifact against the upstream’s published checksum (or signature) before it becomes available to downstream builds. Otherwise, the mirror will faithfully cache a compromised artifact and serve it to every project.
-
Do not skip dependency verification because "it’s internal."
verification-metadata.xmlshould cover artifacts served from the mirror the same way it covers anything else. The mirror is a distribution mechanism, not a trust decision. -
Restrict who can publish to the mirror. The mirror’s write ACL determines who can inject artifacts into every build in the organization. Treat it as production infrastructure.
There are three ways to point Gradle at a mirror, in order of preference.
1. Declare the Mirror in settings.gradle(.kts)
The direct approach. Every build in the repository sees the mirror; nothing else can add a public repository if FAIL_ON_PROJECT_REPOS is set:
pluginManagement {
repositories {
maven {
name = "internalMirror"
url = uri("https://repo.internal.acme.com/gradle-plugins")
credentials(PasswordCredentials::class)
}
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
maven {
name = "internalMirror"
url = uri("https://repo.internal.acme.com/maven-public")
credentials(PasswordCredentials::class)
}
}
}
pluginManagement {
repositories {
maven {
name = 'internalMirror'
url = uri('https://repo.internal.acme.com/gradle-plugins')
credentials(PasswordCredentials)
}
}
}
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
maven {
name = 'internalMirror'
url = uri('https://repo.internal.acme.com/maven-public')
credentials(PasswordCredentials)
}
}
}
This is the pattern to prefer for any repository you control.
The routing decision lives in settings.gradle(.kts) where every reviewer sees it, and content filtering applies to the mirror the same way it applies to any other repository.
2. Redirect Existing Builds via an Init Script
When you cannot or don’t want to modify every project’s settings.gradle(.kts), for example, because you’re rolling out a mirror across an organization with many existing repositories, an init script in ~/.gradle/init.d/ can rewrite repositories at initialization time.
val mirrorUrl = "https://repo.internal.acme.com/maven-public"
settingsEvaluated {
pluginManagement.repositories {
clear()
maven {
name = "internalMirror"
url = uri(mirrorUrl)
}
}
dependencyResolutionManagement.repositories {
clear()
maven {
name = "internalMirror"
url = uri(mirrorUrl)
}
}
}
allprojects {
repositories {
// Replace project-declared repositories with the mirror
val allRepos = toList()
clear()
allRepos.forEach { repo ->
if (repo is MavenArtifactRepository) {
maven {
name = repo.name
url = uri(mirrorUrl)
}
} else {
add(repo)
}
}
}
}
This pattern has security costs:
-
The init script is an implicit trust anchor. Every build on a machine that has this file runs it, see the guidance on init script trust in Securing the Build Tool. Deploy the init script through your existing configuration management, not by asking developers to
curlit into~/.gradle/init.d. -
The script silently overrides what build scripts declare. A developer reading
build.gradle.ktswill not see the mirror; a Build Scan will. Communicate the rewrite clearly, or expect confusion when local behavior diverges from CI.
Prefer this pattern only when option 1 is not feasible, and revisit whenever you can move projects to explicit settings-level declarations.
3. Reuse Maven settings.xml Mirrors
Organizations already invested in Maven often maintain their mirror configuration in ~/.m2/settings.xml.
Since 8.13, Gradle can read those same mirrors instead of requiring the configuration to be duplicated (see Reusing mirror settings from Maven for the full behavior, including mirrorOf matching rules and unsupported features):
org.gradle.mirror.maven.settings=true
| This is an incubating feature and is disabled by default. |
With the property set, Gradle reads mirrors from the user and global settings.xml, matches each declared Maven repository against them using Maven’s mirrorOf grammar, and resolves from the mirror instead.
Credentials come from the <server> entry whose id matches the mirror id.
This is the right choice only if you already have a working Maven mirror setup and want Gradle to honor the same routing. It is not a general-purpose redirection mechanism, and it applies to Maven repositories only. S3, GCS, local file, and Ivy repositories pass through unchanged.
Two properties matter for the security posture of a settings.xml-driven setup:
-
settings.xmlbecomes part of your trust chain. Anyone who can write to~/.m2/settings.xmlcan silently redirect every Gradle build on the machine. Treat the file with the same care as~/.gradle/gradle.properties. -
Publishing is not affected. Mirrors apply to resolution only. Publishing always uploads to the URL declared in the build. This is the safe default, since you want publishes to hit the real destination, not the mirror. Confirm it anyway when auditing the flow.
Require HTTPS
Since Gradle 7, plaintext HTTP repositories are rejected by default.
When Gradle encounters a repository whose URL is not HTTPS (or an equivalent secure scheme), it throws InsecureProtocolException and refuses to resolve dependencies from it.
The relevant flag is allowInsecureProtocol on any MavenArtifactRepository or IvyArtifactRepository.
Its default is false.
dependencyResolutionManagement {
repositories {
maven {
url = uri("http://repo.example.com/maven")
isAllowInsecureProtocol = true // required to make Gradle accept HTTP, but you shouldn't
}
}
}
dependencyResolutionManagement {
repositories {
maven {
url = uri('http://repo.example.com/maven')
allowInsecureProtocol = true // required to make Gradle accept HTTP, but you shouldn't
}
}
}
Setting isAllowInsecureProtocol = true is a security downgrade, anyone on the network path between your build and the repository can intercept the artifacts you download and substitute their own.
Use HTTPS.
If a legacy internal repository is HTTP-only, put a TLS-terminating proxy in front of it and update distributionUrl to the HTTPS endpoint.
Enabling allowInsecureProtocol is not a solution; it is a permanent widening of your attack surface.
Avoid Dynamic Versions
Two patterns weaken repository trust even when the repository itself is legitimate:
-
Dynamic versions (
1.+,latest.release) let the repository decide what your build compiles against. A malicious or mistaken publish upstream reaches your build on the next resolution. Pin to exact versions. -
Anonymous repositories that require credentials accept whatever the network delivers. Always use HTTPS. If a repository requires credentials, provide them through Gradle credentials, not by embedding them in build scripts.
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
maven {
name = "internal"
url = uri("https://repo.internal.acme.com/maven")
credentials(PasswordCredentials::class)
}
}
}
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
maven {
name = 'internal'
url = uri('https://repo.internal.acme.com/maven')
credentials(PasswordCredentials)
}
}
}
With this declaration, Gradle reads the credentials from gradle.properties (or Gradle-managed credential storage) as internalUsername and internalPassword, matching the repository’s name.
No secrets appear in the build script.
Three credential types are supported on AuthenticationSupported repositories:
-
PasswordCredentials: reads<repoName>Username/<repoName>Password. -
HttpHeaderCredentials: reads<repoName>AuthHeaderName/<repoName>AuthHeaderValue. Useful for repositories that authenticate via a header (for example, a bearer token). -
AwsCredentials: reads<repoName>AccessKey/<repoName>SecretKey/<repoName>SessionToken. Useful for S3-backed Maven repositories.
Configure metadataSources Deliberately
A repository declaration exposes a metadataSources block that controls how Gradle resolves module metadata:
maven {
url = uri("https://repo.internal.acme.com/maven")
metadataSources {
mavenPom()
gradleMetadata()
// artifact() // NOT enabled by default; enables filename-based resolution
}
}
maven {
url = uri('https://repo.internal.acme.com/maven')
metadataSources {
mavenPom()
gradleMetadata()
// artifact() // NOT enabled by default; enables filename-based resolution
}
}
The important security lever here is the artifact() source:
-
mavenPom()/ivyDescriptor()/gradleMetadata()all require a metadata descriptor that pins the artifact to its coordinates, its dependencies, and (with Gradle Module Metadata) additional integrity data. -
artifact()tells Gradle it may resolve a coordinate by asking the repository whether a matching file exists, with no metadata descriptor. There is no POM, no.module, and no validation that the file at that path is actually the artifact those coordinates describe.
Do not enable artifact() on repositories that host third-party or public content.
Reserve it for tightly controlled internal locations where you accept the risk of coordinate-only resolution.
Filter Repository Content
By default, when a build resolves a dependency, Gradle asks every declared repository in order until one answers. That default is convenient but it has two security consequences worth understanding.
First, every repository you declare sees the full list of coordinates your build asks for.
If you declare a public repository alongside a private one, the public repository learns the names of your private artifacts every time it is queried.
This is called dependency name leakage, and it is a real vector for dependency confusion attacks.
An attacker who knows the name of your internal com.acme.secret-tool package can publish a package of the same name to a public repository and wait for a misconfigured build to pick it up.
Second, an artifact you expected to come from a trusted repository can silently come from a less trusted one if the trusted repository is briefly unavailable or if resolution order changes.
Content filtering solves both problems by declaring, up front, which coordinates each repository is allowed to serve. See Filtering Repository Content for the full reference including Maven-repository-specific filters and Ivy dynamic resolve mode.
Restrict Groups to Repositories
Every ArtifactRepository exposes a content { … } block.
Inside that block you declare which groups, modules, versions, or configurations the repository is allowed to serve.
Filters apply before Gradle contacts the repository, so the repository never sees requests for coordinates it isn’t allowed to serve.
The available filters are:
| Filter | Effect |
|---|---|
|
Match a single exact group. |
|
Match groups by regular expression. |
|
Match a group and its dot-delimited subgroups. |
|
Match |
|
Match |
|
Match a specific |
|
Match |
|
Restrict which configurations can resolve from this repository. |
A typical routing configuration:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral {
content {
excludeGroupAndSubgroups("com.acme")
}
}
maven {
name = "internal"
url = uri("https://repo.internal.acme.com/maven")
credentials(PasswordCredentials::class)
content {
includeGroupAndSubgroups("com.acme")
}
}
}
}
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral {
content {
excludeGroupAndSubgroups('com.acme')
}
}
maven {
name = 'internal'
url = uri('https://repo.internal.acme.com/maven')
credentials(PasswordCredentials)
content {
includeGroupAndSubgroups('com.acme')
}
}
}
}
With this configuration:
-
Any request for a
com.acme*artifact is sent only to the internal repository. -
Any request for anything else is sent only to Maven Central.
-
Neither repository is ever asked about coordinates it isn’t allowed to serve, so neither repository can learn the names of coordinates it shouldn’t see.
Prefer includeGroupAndSubgroups and excludeGroupAndSubgroups over includeGroup / excludeGroup when your organization owns a group and its subgroups (e.g. com.acme, com.acme.web, com.acme.mobile). Filtering by exact group only would leave subgroups uncovered.
|
Make a Repository Exclusive for a Set of Coordinates
When an artifact must come from one specific repository and nowhere else, declare that repository as exclusive for those coordinates using ExclusiveContentRepository:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
exclusiveContent {
forRepository {
maven {
name = "internal"
url = uri("https://repo.internal.acme.com/maven")
credentials(PasswordCredentials::class)
}
}
filter {
includeGroupAndSubgroups("com.acme")
}
}
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
exclusiveContent {
forRepository {
maven {
name = 'internal'
url = uri('https://repo.internal.acme.com/maven')
credentials(PasswordCredentials)
}
}
filter {
includeGroupAndSubgroups('com.acme')
}
}
mavenCentral()
}
}
exclusiveContent is stronger than a plain include filter:
-
Coordinates matched by the filter are resolved only from the exclusive repository.
-
No other repository (even one that would normally match the same coordinates) is ever asked.
-
The
filter { }block accepts only inclusive rules (includeGroup,includeGroupAndSubgroups,includeModule, etc.). Exclusions are implicit: everything not included is excluded.
Use exclusiveContent for internal artifacts, mirrors that host a specific project, or any coordinate that must never be sourced from a public mirror under any circumstances.
|
If you use |