Skip to content

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

kind create cluster --name opendepot

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:

kubectl get pods -n opendepot-system

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:

kubectl port-forward svc/server 8080:80 -n opendepot-system

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:

curl http://opendepot.localtest.me:8080/.well-known/terraform.json

Expected output:

{"modules.v1":"/opendepot/modules/v1/"}

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:

kubectl get versions -n opendepot-system -w

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):

kubectl get versions -n opendepot-system -w

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):

kubectl get provider aws \
  -n opendepot-system \
  -o jsonpath='{.status.sourceScan}' | jq .
{
  "scannedAt": "2026-05-03T02:12:05Z",
  "version": "5.80.0",
  "findings": []
}

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:

kubectl patch version aws-5-80-0-linux-amd64 -n opendepot-system \
  --type merge -p '{"spec":{"forceSync":true}}'

Cleanup

Stop the port-forward and delete the Kind cluster:

kubectl port-forward svc/server 8080:80 -n opendepot-system
kind delete cluster --name opendepot