A simple Maven repository mirror server built with Spring Boot. Kagami (鏡, meaning "mirror" in Japanese) provides efficient caching and proxying of Maven artifacts from multiple remote repositories.
- Local Caching: Automatically caches artifacts from remote repositories to reduce download times
- Multiple Repository Support: Configure multiple remote repositories with individual settings
- Private Repository Support: JWT-based authentication for secure repository access
- REST API: Simple REST endpoints for artifact retrieval and cache management
- Web Dashboard: Modern React-based UI for repository browsing and management with unified header navigation
- Authentication: Form-based authentication or OIDC/OAuth2 login for web UI access with styled login/logout pages
- Token Management: Web-based JWT token generation with configurable expiration, permissions, and build tool configuration examples
- User Interface: Consistent header across all pages showing logged-in username, logout functionality, and token generation access
- Security Features: OAuth2 Resource Server with JWT tokens, repository-specific access control, role-based token generation, CSRF protection partially disabled for API usage
- OIDC Support: OpenID Connect authentication with multiple identity providers (Google, Microsoft Entra ID, etc.)
The fastest way to get started with Kagami is using the pre-built Docker image:
# Run Kagami with Docker
docker run --rm --pull always -p 8080:8080 \
-v /tmp/kagami:/var/kagami/storage \
-e kagami.storage.path=/var/kagami/storage \
-e kagami.repositories.central.url=https://repo.maven.apache.org/maven2 \
ghcr.io/making/kagami:jvmAccess the application:
- Open http://localhost:8080 in your browser
- Log in with default credentials:
- Username:
demo - Password:
demo
- Username:
- Access artifacts:
wget http://localhost:8080/artifacts/central/org/springframework/spring-core/6.0.0/spring-core-6.0.0.jar
kagami.jwt.private-key and kagami.jwt.public-key to enable JWT token functionality. See Private Repository Configuration for key generation instructions. The Docker Compose example below shows the recommended production setup.
Docker Configuration Options:
# With private repository with authentication
docker run --rm --pull always -p 8080:8080 \
-v /tmp/kagami:/var/kagami/storage \
-e kagami.storage.path=/var/kagami/storage \
-e kagami.repositories.spring-enterprise.url=https://packages.broadcom.com/artifactory/spring-enterprise \
-e kagami.repositories.spring-enterprise.username=your-bc-username \
-e kagami.repositories.spring-enterprise.password=your-bc-token \
-e kagami.repositories.spring-enterprise.is-private=true \
ghcr.io/making/kagami:jvm
# With custom authentication
docker run --rm --pull always -p 8080:8080 \
-v /tmp/kagami:/var/kagami/storage \
-e kagami.storage.path=/var/kagami/storage \
-e kagami.repositories.central.url=https://repo.maven.apache.org/maven2 \
-e spring.security.user.name=admin \
-e spring.security.user.password='{noop}mypassword' \
ghcr.io/making/kagami:jvmUsing Docker Compose:
For production deployments with JWT token support, create a docker-compose.yml file:
services:
kagami:
image: ghcr.io/making/kagami:jvm
pull_policy: always
ports:
- "8080:8080"
volumes:
- ./kagami-storage:/var/kagami/storage
- ./kagami-private.pem:/etc/kagami/kagami-private.pem:ro
- ./kagami-public.pem:/etc/kagami/kagami-public.pem:ro
environment:
kagami.storage.path: /var/kagami/storage
kagami.repositories.central.url: https://repo.maven.apache.org/maven2
# Configure private repository (requires JWT keys)
kagami.repositories.private-repo.url: https://internal.repo.com/maven2
kagami.repositories.private-repo.is-private: true
# JWT key configuration
kagami.jwt.private-key: file:/etc/kagami/kagami-private.pem
kagami.jwt.public-key: file:/etc/kagami/kagami-public.pem
# Optional: Custom authentication
# spring.security.user.name: admin
# spring.security.user.password: '{noop}mypassword'Before running:
- Generate JWT key pair following the instructions in Private Repository Configuration
- Place
kagami-private.pemandkagami-public.pemin the same directory as yourdocker-compose.yml - Set appropriate file permissions:
mkdir -p ./kagami-storage chmod a+w ./kagami-storage chmod a+r ./kagami-private.pem ./kagami-public.pem
- Run with:
docker compose up -d
- Java 21 or later
- Clone the repository:
git clone https://github.com/making/kagami.git
cd kagami- Build and run:
./mvnw spring-boot:run- Access the web dashboard:
open http://localhost:8080-
Log in to the web dashboard:
- Default username:
demo - Default password:
demo
- Default username:
-
Access artifacts via HTTP:
wget http://localhost:8080/artifacts/central/org/springframework/spring-core/6.0.0/spring-core-6.0.0.jarConfigure repositories and settings in application.properties or environment variables:
# Storage path for cached artifacts
kagami.storage.path=/var/kagami/storage
# Public repositories
kagami.repositories.central.url=https://repo.maven.apache.org/maven2
kagami.repositories.jcenter.url=https://jcenter.bintray.com# Private repository with Basic authentication
kagami.repositories.private.url=https://private.repo.example.com/maven2
kagami.repositories.private.username=your-username
kagami.repositories.private.password=your-password# Mark repository as private (requires JWT authentication)
kagami.repositories.private-repo.url=https://internal.repo.com/maven2
kagami.repositories.private-repo.is-private=true
# JWT key pair configuration
kagami.jwt.private-key=classpath:kagami-private.pem
kagami.jwt.public-key=classpath:kagami-public.pemTo generate the JWT key pair, run the following commands:
# Generate RSA private key
openssl genrsa -out private.pem 2048
# Extract public key
openssl rsa -in private.pem -outform PEM -pubout -out kagami-public.pem
# Convert private key to PKCS#8 format
openssl pkcs8 -topk8 -inform PEM -in private.pem -out kagami-private.pem -nocrypt
# Clean up temporary file
rm -f private.pemPlace the generated kagami-private.pem and kagami-public.pem files in your src/main/resources directory.
Besides file references, you can configure JWT keys using the following methods:
File path (recommended for Docker/containers):
kagami.jwt.private-key=file:/path/to/kagami-private.pem
kagami.jwt.public-key=file:/path/to/kagami-public.pemBase64 encoded strings (useful when file mounting is difficult):
kagami.jwt.private-key=base64:LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0t...
kagami.jwt.public-key=base64:LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0K...To generate base64 encoded values:
# For private key
echo "kagami.jwt.private-key=base64:$(cat kagami-private.pem | base64 -w0)"
# For public key
echo "kagami.jwt.public-key=base64:$(cat kagami-public.pem | base64 -w0)"The base64 format is particularly useful in container environments where file mounting is challenging, such as certain cloud platforms like Cloud Foundry.
For more information on configuration value conversion, see the Spring Boot documentation.
# Configure web UI authentication (default: demo/demo)
kagami.authentication.type=simple
spring.security.user.name=your-username
spring.security.user.password={noop}plainpassword
# For production, use encoded password:
# spring.security.user.password={bcrypt}$2a$10$...Only one user can be configured with this method. For multiple users, use OIDC authentication.
Configure OIDC authentication for enterprise single sign-on:
# Enable OIDC authentication
kagami.authentication.type=oidc
# Restrict access to specific email patterns (regex)
kagami.authentication.allowed-name-patterns=.*@example\\.com,.*@example\\.org
# Configure Google as identity provider
spring.security.oauth2.client.provider.google.issuer-uri=https://accounts.google.com
spring.security.oauth2.client.provider.google.user-name-attribute=email
spring.security.oauth2.client.registration.google.client-id=your-google-client-id
spring.security.oauth2.client.registration.google.client-secret=your-google-client-secret
spring.security.oauth2.client.registration.google.client-name=Google
spring.security.oauth2.client.registration.google.scope=openid,email
# Configure Microsoft Entra ID (formerly Azure AD)
spring.security.oauth2.client.provider.microsoft-entra-id.issuer-uri=https://sts.windows.net/{tenant-id}/
spring.security.oauth2.client.provider.microsoft-entra-id.user-name-attribute=email
spring.security.oauth2.client.registration.microsoft-entra-id.client-id=your-client-id
spring.security.oauth2.client.registration.microsoft-entra-id.client-secret=your-client-secret
spring.security.oauth2.client.registration.microsoft-entra-id.client-name=Microsoft Entra ID
spring.security.oauth2.client.registration.microsoft-entra-id.scope=openid,emailNotes:
- When OIDC is enabled, users will see provider-specific login buttons instead of username/password fields
- The
allowed-name-patternsproperty restricts access to users whose name matches the specified patterns - Multiple identity providers can be configured simultaneously
- Users must have matching email patterns to be granted USER role access
See the Spring Boot documentation for more details on configuring OIDC authentication.
# Configure HTTP proxy
kagami.proxy.url=http://proxy.company.com:8080Alternatively, use environment variables:
export http_proxy=http://proxy.company.com:8080
export HTTP_PROXY=http://proxy.company.com:8080See the API documentation for details on available endpoints.
Configure your Maven settings to use Kagami as a mirror:
<settings>
<profiles>
<profile>
<id>kagami-public</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<repositories>
<repository>
<id>kagami-central</id>
<name>Kagami Public Repository</name>
<url>http://localhost:8080/artifacts/central</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>kagami-central</id>
<name>Kagami Public Repository</name>
<url>http://localhost:8080/artifacts/central</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
<!-- ALTERNATIVE: Mirror configuration -->
<!-- Use mirrors when you want to redirect ALL Maven repository requests through Kagami -->
<!-- This is useful for: -->
<!-- - Corporate environments where all external access must go through a proxy -->
<!-- - Offline environments where only Kagami has access to external repositories -->
<!-- - Performance optimization when Kagami has better network access to upstream repos -->
<!--
<mirrors>
<mirror>
<id>kagami</id>
<mirrorOf>*</mirrorOf>
<name>Kagami Mirror</name>
<url>http://localhost:8080/artifacts/central</url>
</mirror>
</mirrors>
-->
</settings>For private repositories, generate a JWT token using the web interface:
- Log in to the web dashboard at
http://localhost:8080 - Click "Generate Token" in the header navigation
- Select repositories and permissions using checkboxes
- Set token expiration with human-friendly units (default: 6 months)
- Click "Generate Token" and copy the generated JWT
- Use the provided Maven, Gradle Groovy, or Gradle Kotlin configuration examples
Note: JWT tokens cannot be used to generate new tokens. You must use the web interface.
Two authentication methods are supported for build tools:
- Username/Password (Basic authentication): the username can be any value and the password is the JWT token
- Bearer token: send the JWT token in the
Authorization: Bearerheader
Then configure Maven with the JWT token. With the standard username/password configuration:
<settings>
<servers>
<server>
<id>kagami-private</id>
<username>any-username</username>
<password>YOUR_JWT_TOKEN</password>
</server>
</servers>
<profiles>
<profile>
<id>kagami-private</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<repositories>
<repository>
<id>kagami-private</id>
<name>Kagami Private Repository</name>
<url>http://localhost:8080/artifacts/private-repo</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>kagami-private</id>
<name>Kagami Private Repository</name>
<url>http://localhost:8080/artifacts/private-repo</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
<!-- ALTERNATIVE: Mirror configuration -->
<!-- Use mirrors when you want to redirect ALL Maven repository requests through Kagami -->
<!-- This is useful for: -->
<!-- - Corporate environments where all external access must go through a proxy -->
<!-- - Offline environments where only Kagami has access to external repositories -->
<!-- - Performance optimization when Kagami has better network access to upstream repos -->
<!--
<mirrors>
<mirror>
<id>kagami-private</id>
<mirrorOf>*</mirrorOf>
<name>Kagami Private Mirror</name>
<url>http://localhost:8080/artifacts/private-repo</url>
</mirror>
</mirrors>
-->
</settings>Alternatively, configure the Bearer token method by replacing the <server> element above with:
<server>
<id>kagami-private</id>
<configuration>
<httpHeaders>
<property>
<name>Authorization</name>
<value>Bearer YOUR_JWT_TOKEN</value>
</property>
</httpHeaders>
</configuration>
</server>repositories {
maven {
url 'http://localhost:8080/artifacts/central'
}
}The examples below use the username/password method. The Bearer token method is also available (see the end of this section).
Groovy DSL ($HOME/.gradle/init.gradle)
// $HOME/.gradle/init.gradle - Groovy DSL version
def repoUrl = "http://localhost:8080/artifacts/private-repo"
def repoToken = "YOUR_JWT_TOKEN"
// For regular dependencies (legacy projects)
allprojects {
repositories {
maven {
url = repoUrl
allowInsecureProtocol = true
credentials {
username = "kagami" // can be any value
password = repoToken
}
}
mavenCentral() // fallback
}
}
// Configure settings.gradle
settingsEvaluated { settings ->
// For plugin resolution
settings.pluginManagement {
repositories {
maven {
url = repoUrl
allowInsecureProtocol = true
credentials {
username = "kagami" // can be any value
password = repoToken
}
}
gradlePluginPortal() // fallback
mavenCentral() // fallback
}
}
// Dependency resolution management (Gradle 6.8+)
settings.dependencyResolutionManagement {
// Ignore repositories defined in build.gradle
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
maven {
url = repoUrl
allowInsecureProtocol = true
credentials {
username = "kagami" // can be any value
password = repoToken
}
}
mavenCentral()
}
}
}Kotlin DSL ($HOME/.gradle/init.gradle.kts)
// $HOME/.gradle/init.gradle.kts - Kotlin DSL version
val repoUrl = "http://localhost:8080/artifacts/private-repo"
val repoToken = "YOUR_JWT_TOKEN"
// Extension function to configure repository
fun RepositoryHandler.addKagamiRepository() {
maven {
url = uri(repoUrl)
isAllowInsecureProtocol = true
credentials {
username = "kagami" // can be any value
password = repoToken
}
}
}
// For regular dependencies (legacy projects)
allprojects {
repositories {
addKagamiRepository()
mavenCentral() // fallback
}
}
// Configure settings.gradle
settingsEvaluated {
// For plugin resolution
pluginManagement {
repositories {
addKagamiRepository()
gradlePluginPortal() // fallback
mavenCentral() // fallback
}
}
// Dependency resolution management (Gradle 6.8+)
dependencyResolutionManagement {
// Ignore repositories defined in build.gradle
@Suppress("UnstableApiUsage")
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
addKagamiRepository()
mavenCentral() // fallback
}
}
}To use the Bearer token method instead, replace each credentials block in the examples above
with the following.
Groovy DSL:
authentication {
header(HttpHeaderAuthentication)
}
credentials(HttpHeaderCredentials) {
name = "Authorization"
value = "Bearer YOUR_JWT_TOKEN"
}Kotlin DSL (add the imports at the top of the init script):
import org.gradle.authentication.http.HttpHeaderAuthentication
import org.gradle.kotlin.dsl.*
// ...
authentication {
create<HttpHeaderAuthentication>("header")
}
credentials(HttpHeaderCredentials::class) {
name = "Authorization"
value = "Bearer YOUR_JWT_TOKEN"
}Generate the JWT key pair as documented above and paste base64-encoded values in manifest.yaml as below:
applications:
- name: kagami
instances: 1
memory: 768m
docker:
image: ghcr.io/making/kagami:jvm
env:
kagami.repositories.central.url: https://repo.maven.apache.org/maven2
kagami.jwt.private-key: base64:LS0tLS1CRUd...
kagami.jwt.public-key: base64:LS0tLS1CRUdJ...Then deploy it:
cf push
On Cloud Foundry, applications are ephemeral by default. Therefore, all data will be lost when Kagami is restarted or updated. Kagami is a mirror repository, so even if data is lost, it will be re-downloaded the next time it is accessed. Aside from the slow initial download time, ephemeral containers are not a problem. Similarly, when scaling out, each container has its own cache with shared nothing. If initial download time or disk capacity are issues, consider using Volume Services.
# Build the application
./mvnw clean package
# Run all tests
./mvnw test
# Run with development profile
./mvnw spring-boot:runKagami supports creating optimized Docker images using Spring Boot's buildpacks integration:
# Create Docker image using buildpacks (requires Docker)
./mvnw spring-boot:build-image
# The generated image will be tagged as: kagami:0.0.1-SNAPSHOT
# You can run it with:
docker run --pull always -p 8080:8080 \
-v /tmp/kagami:/var/kagami/storage \
-e kagami.storage.path=/var/kagami/storage \
-e kagami.repositories.central.url=https://repo.maven.apache.org/maven2 \
kagami:0.0.1-SNAPSHOT
# Custom image name and tag
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myregistry/kagami:latestEnable debug logging for troubleshooting:
logging.level.am.ik.kagami=DEBUG
logging.level.org.eclipse.aether=DEBUG
# For security troubleshooting:
logging.level.org.springframework.security=DEBUGThe application provides health check endpoints via Spring Actuator:
# Health status
curl http://localhost:8080/actuator/health
# Prometheus metrics
curl http://localhost:8080/actuator/prometheusThe following features are planned for future releases:
- S3 Storage: Amazon S3 and S3-compatible storage backends (MinIO, etc.)
Licensed under the Apache License, Version 2.0. See LICENSE for details.