Local Quickstart (kind)¶
The fastest way to try OpenDepot is with a local kind cluster using the filesystem storage backend and hostPath. This avoids any cloud provider setup — no S3 bucket, no Azure Storage Account, no credentials, no ingress controller, and no TLS certificates. You'll have a fully functional registry in minutes using kubectl port-forward and the public *.localtest.me DNS service (all *.localtest.me hostnames resolve to 127.0.0.1).
Note
OpenTofu and Terraform require module registry hostnames to contain at least one dot. localhost alone is not valid. opendepot.localtest.me resolves to 127.0.0.1 via public DNS, making it a convenient dotted hostname for local testing without editing /etc/hosts or installing any ingress controller.
Prerequisites¶
Step 1: Create the Cluster¶
Step 2: Deploy with Helm¶
helm repo add opendepot https://tonedefdev.github.io/opendepot
helm repo update
helm install opendepot opendepot/opendepot \
-n opendepot-system --create-namespace \
--set storage.filesystem.enabled=true \
--set storage.filesystem.hostPath=/data/modules \
--set server.anonymousAuth=true \
--wait
Verify all pods are running:
Note
Apple Silicon users: If building from source, the default PLATFORM is linux/arm64. For Intel Macs or Linux, run make deploy PLATFORM=linux/amd64.
Step 3: Port-Forward the Server¶
In a separate terminal, forward the OpenDepot server to a local port:
The server is now reachable at http://opendepot.localtest.me:8080 — no ingress controller or TLS certificate required. OpenTofu will resolve opendepot.localtest.me to 127.0.0.1 via public DNS and connect through the port-forward.
Verify service discovery is working:
Expected output:
Step 4: Create a Test Module¶
Apply a Module resource that pulls a small public module from GitHub:
cat <<EOF | kubectl apply -f -
apiVersion: opendepot.defdev.io/v1alpha1
kind: Module
metadata:
name: terraform-aws-s3-bucket
namespace: opendepot-system
spec:
moduleConfig:
provider: aws
repoOwner: terraform-aws-modules
repoUrl: https://github.com/terraform-aws-modules/terraform-aws-s3-bucket
fileFormat: zip
storageConfig:
fileSystem:
directoryPath: /data/modules
versions:
- version: "4.3.0"
EOF
Note
The Module CR name (terraform-aws-s3-bucket) must match the GitHub repository name, because the module controller uses it as the repository name when fetching archives if spec.moduleConfig.name is omitted.
Watch the Version resource sync:
Once SYNCED shows true, the module archive has been fetched from GitHub and stored in the local filesystem.
Step 5: Use the Registry with OpenTofu¶
Create a working directory with a Terraform/OpenTofu config and a .tofurc (or .terraformrc) that points OpenTofu at your local registry:
mkdir /tmp/opendepot-test && cd /tmp/opendepot-test
cat > main.tf <<'EOF'
module "s3_bucket" {
source = "opendepot.localtest.me/opendepot-system/terraform-aws-s3-bucket/aws"
version = "4.3.0"
}
EOF
cat > .tofurc <<'EOF'
host "opendepot.localtest.me" {
services = {
"modules.v1" = "http://opendepot.localtest.me:8080/opendepot/modules/v1/"
}
}
EOF
TF_CLI_CONFIG_FILE=.tofurc tofu init
The .tofurc host block overrides the default HTTPS protocol discovery for this hostname, allowing plain HTTP over the port-forward. The host block key is the bare hostname without a port; the port belongs only in the services URL value. You should see OpenTofu download the module from your local OpenDepot instance:
Initializing modules...
Downloading opendepot.localtest.me/opendepot-system/terraform-aws-s3-bucket/aws 4.3.0 for s3_bucket...
- s3_bucket in .terraform/modules/s3_bucket
OpenTofu has been successfully initialized!
Step 6: (Optional) Test with Authentication¶
To test OpenDepot's Kubernetes-native auth, redeploy with anonymousAuth disabled:
helm upgrade opendepot opendepot/opendepot \
-n opendepot-system \
--reuse-values \
--set server.anonymousAuth=false \
--set server.useBearerToken=true \
--wait
Create a ServiceAccount and bind it to a read-only role:
kubectl create serviceaccount test-user -n opendepot-system
kubectl create role opendepot-reader -n opendepot-system \
--resource=modules.opendepot.defdev.io,versions.opendepot.defdev.io,providers.opendepot.defdev.io \
--verb=get,list,watch
kubectl create rolebinding test-user-reader -n opendepot-system \
--role=opendepot-reader \
--serviceaccount=opendepot-system:test-user
Generate a short-lived token and pass it via the TF_TOKEN_* environment variable. OpenTofu maps the variable name back to a hostname by replacing each _ with . (lowercased), so TF_TOKEN_OPENDEPOT_LOCALTEST_ME supplies credentials for opendepot.localtest.me. The existing .tofurc from Step 5 already has the required host block — no changes to it are needed:
TOKEN=$(kubectl create token test-user -n opendepot-system --duration=1h)
TF_TOKEN_OPENDEPOT_LOCALTEST_ME="$TOKEN" TF_CLI_CONFIG_FILE=.tofurc tofu init
OpenTofu sends the bearer token to OpenDepot, which forwards it to the Kubernetes API for authentication and RBAC authorization. This is the same flow used in production — no separate user database or API keys required.
Step 7: (Optional) Test with a Depot¶
To test automatic version discovery from GitHub:
cat <<EOF | kubectl apply -f -
apiVersion: opendepot.defdev.io/v1alpha1
kind: Depot
metadata:
name: test-depot
namespace: opendepot-system
spec:
global:
moduleConfig:
fileFormat: zip
storageConfig:
fileSystem:
directoryPath: /data/modules
moduleConfigs:
- name: terraform-aws-s3-bucket
provider: aws
repoOwner: terraform-aws-modules
versionConstraints: ">= 4.3.0, <= 4.4.0"
providerConfigs:
- name: random
operatingSystems:
- linux
architectures:
- amd64
versionConstraints: "= 3.6.0"
storageConfig:
fileSystem:
directoryPath: /data/modules
EOF
The Depot controller queries GitHub releases for modules and the HashiCorp Releases API for providers, creates Module and Provider resources for matching versions, and the pipeline syncs them to local storage automatically.
Step 8: (Optional) Test with a Provider¶
Providers are synced from the HashiCorp Releases API and served via the Terraform Provider Registry Protocol. Provider binaries can be large (the aws provider for a single OS/arch is ~700 MB), so this step is optional.
Step 8a: Generate a GPG key for provider signing
OpenTofu verifies a GPG signature over the SHA256SUMS file when installing a provider. Generate a dedicated key and store it as a Kubernetes Secret:
# Generate a key (no passphrase, batch mode)
gpg --batch --gen-key <<EOF
Key-Type: RSA
Key-Length: 4096
Name-Real: OpenDepot Local
Name-Email: opendepot@local.test
Expire-Date: 0
%no-protection
EOF
KEY_ID=$(gpg --list-keys --with-colons opendepot@local.test | awk -F: '/^pub/{print $5}' | tail -1)
ASCII_ARMOR=$(gpg --armor --export "$KEY_ID")
PRIVATE_B64=$(gpg --armor --export-secret-keys "$KEY_ID" | base64 | tr -d '\n')
kubectl create secret generic opendepot-provider-gpg \
--namespace opendepot-system \
--from-literal=OPENDEPOT_PROVIDER_GPG_KEY_ID="$KEY_ID" \
--from-literal=OPENDEPOT_PROVIDER_GPG_ASCII_ARMOR="$ASCII_ARMOR" \
--from-literal=OPENDEPOT_PROVIDER_GPG_PRIVATE_KEY_BASE64="$PRIVATE_B64"
Step 8b: Redeploy OpenDepot with the provider controller and GPG secret
helm upgrade opendepot opendepot/opendepot \
-n opendepot-system \
--reuse-values \
--set provider.enabled=true \
--set server.gpg.secretName=opendepot-provider-gpg \
--wait
Step 8c: Create a Provider resource
cat <<EOF | kubectl apply -f -
apiVersion: opendepot.defdev.io/v1alpha1
kind: Provider
metadata:
name: aws
namespace: opendepot-system
spec:
providerConfig:
name: aws
operatingSystems:
- linux
architectures:
- amd64
storageConfig:
fileSystem:
directoryPath: /data/modules
versions:
- version: "5.80.0"
EOF
Watch the Version resource sync (this downloads ~700 MB from HashiCorp):
Once SYNCED shows true, the provider binary is stored in the local filesystem.
Step 8d: Use the provider registry with OpenTofu
mkdir /tmp/opendepot-provider-test && cd /tmp/opendepot-provider-test
cat > main.tf <<'EOF'
terraform {
required_providers {
aws = {
source = "opendepot.localtest.me:8080/opendepot-system/aws"
version = "5.80.0"
}
}
}
EOF
cat > .tofurc <<'EOF'
host "opendepot.localtest.me:8080" {
services = {
"providers.v1" = "http://opendepot.localtest.me:8080/opendepot/providers/v1/"
}
}
EOF
TF_CLI_CONFIG_FILE=.tofurc tofu init
The .tofurc host block overrides HTTPS protocol discovery for this hostname, allowing plain HTTP over the port-forward. OpenTofu will resolve opendepot.localtest.me to 127.0.0.1 and install the provider from your local OpenDepot instance:
Initializing provider plugins...
- Finding opendepot.localtest.me:8080/opendepot-system/aws versions matching "5.80.0"...
- Installing opendepot.localtest.me:8080/opendepot-system/aws v5.80.0...
- Installed opendepot.localtest.me:8080/opendepot-system/aws v5.80.0
OpenTofu has been successfully initialized!
Step 8d (authenticated): Using the provider registry with bearer token auth
If you enabled authentication in Step 6, providers need a credentials block in .tofurc because the provider source address includes the port (opendepot.localtest.me:8080) and the TF_TOKEN_* env var format does not support ports. Generate a token and write a .tofurc that covers the provider credentials and both host blocks:
TOKEN=$(kubectl create token test-user -n opendepot-system --duration=1h)
cat > /tmp/opendepot-provider-test/.tofurc <<EOF
credentials "opendepot.localtest.me:8080" {
token = "${TOKEN}"
}
host "opendepot.localtest.me" {
services = {
"modules.v1" = "http://opendepot.localtest.me:8080/opendepot/modules/v1/"
}
}
host "opendepot.localtest.me:8080" {
services = {
"providers.v1" = "http://opendepot.localtest.me:8080/opendepot/providers/v1/"
}
}
EOF
TF_TOKEN_OPENDEPOT_LOCALTEST_ME="$TOKEN" TF_CLI_CONFIG_FILE=/tmp/opendepot-provider-test/.tofurc tofu init
Warning
Using token inside a host block is silently ignored by OpenTofu — provider credentials must be in a separate credentials block. The credentials block key must exactly match the hostname as it appears in the source address, including the port. A mismatch means OpenTofu sends no token and the server returns 401.
Step 9: (Optional) Test Trivy Scanning¶
This step shows Trivy scanning in action against the module from Step 4 and, if you completed Step 8, the provider as well.
Step 9a: Enable scanning
Setting scanning.enabled=true activates module IaC scanning with no additional infrastructure — the version-controller automatically uses the -scanning image variant. To also enable provider binary and source scanning (which needs the Trivy DB PVC and CronJob), set scanning.providerScanning=true. Kind uses a single-node cluster, so ReadWriteOnce access mode and the default storage class are sufficient. Set offline=false so Trivy downloads the vulnerability database directly rather than waiting for the CronJob to complete on a fresh cluster:
helm upgrade opendepot opendepot/opendepot \
-n opendepot-system \
--reuse-values \
--set scanning.enabled=true \
--set scanning.providerScanning=true \
--set scanning.offline=false \
--set scanning.cache.accessMode=ReadWriteOnce \
--wait
Note
scanning.offline=false is a convenience for local development. In production, leave offline=true (the default) and rely on the trivy-db-updater CronJob to keep the database current. scanning.offline only applies to provider scanning — module IaC scanning uses bundled config rules and makes no network calls.
Step 9b: Trigger a module scan
Enabling the Trivy scanner forces the Version controller to restart to apply the correct configuration.
Wait for the Version resource to reconcile, then inspect the IaC findings:
kubectl get versions -n opendepot-system -w
# wait for SYNCED=true, then Ctrl-C
kubectl get version terraform-aws-s3-bucket-4.3.0 \
-n opendepot-system \
-o jsonpath='{.status.sourceScan}' | jq .
You should see something like:
{
"scannedAt": "2026-05-03T02:11:00Z",
"findings": [
{
"vulnerabilityID": "AWS-0086",
"pkgName": "aws_s3_bucket",
"installedVersion": "",
"severity": "HIGH",
"title": "S3 Bucket does not have logging enabled"
},
{
"vulnerabilityID": "AWS-0088",
"pkgName": "aws_s3_bucket",
"installedVersion": "",
"severity": "MEDIUM",
"title": "S3 Bucket does not have versioning enabled"
}
]
}
Module IaC findings contain Trivy rule IDs such as AWS-0086 rather than CVE identifiers. An empty findings array means no misconfigurations were detected.
Step 9c: (Requires Step 8) Inspect provider scan results
If you completed Step 8, the provider binary and source scans run automatically once the controller has restarted.
Check the binary scan on the Version resource (per OS/arch):
kubectl get version aws-5-80-0-linux-amd64 \
-n opendepot-system \
-o jsonpath='{.status.binaryScan}' | jq .
{
"scannedAt": "2026-05-03T02:12:00Z",
"findings": [
{
"vulnerabilityID": "CVE-2024-24790",
"pkgName": "stdlib",
"installedVersion": "1.22.3",
"fixedVersion": "1.22.4",
"severity": "CRITICAL",
"title": "net/netip: Unexpected behavior from Is methods for IPv4-mapped IPv6 addresses"
}
]
}
Check the source scan on the Provider resource (shared across all OS/arch variants):
Provider binary findings contain CVE identifiers and package version details. The source scan covers go.mod dependencies — an empty findings array means no vulnerable dependencies were detected.
Note
If status.binaryScan is empty after the controller restarts, the version was already cached from a previous run and the fast-path skipped re-downloading it. Use forceSync: true to trigger a one-time re-download and re-scan:
Cleanup¶
Stop the port-forward and delete the Kind cluster: