Docs
Embedded ClusterStorage

Accessing the In-Cluster Object Store

Credentials, endpoint, and layout for the in-cluster Garage bucket, plus uploading migrated media and plugin wheelhouses.

Available in NetBox Enterprise 2.3.0

This page applies when the File Storage Backend is set to In-Cluster Object Storage. See In-Cluster Object Storage for how to enable it.

With in-cluster object storage, NetBox media and custom scripts live in a Garage bucket rather than on a node-local volume. This page covers reaching that bucket directly — to inspect what is stored, upload media during a migration, or deliver a plugin wheelhouse.

Uploading objects by hand is a supported and sometimes necessary operation. There is one rule that governs whether it works:

NetBox reads its database, not the bucket

An object is served when a database record points at it. So uploading works when the record already exists — migrating media into a database you have just imported, for example. It does not create anything new: a brand-new image or script placed in the bucket with no record behind it stays invisible, because nothing scans the bucket for new files.

See What you can and cannot upload for where the line falls, Migrating media from another NetBox for the migration case, and Delivering a plugin wheelhouse for custom plugins.

What is in the bucket

NetBox uses a single bucket named netbox. Media and scripts share it by key prefix:

PrefixContentsDjango storage backend
media/Image attachments, device-type images, and other uploadsSTORAGES['default']
scripts/Custom script and report modulesSTORAGES['scripts']

Writing outside those two prefixes does nothing — no part of NetBox reads it.

Listing buckets shows a second one, nbe-health. NetBox Enterprise uses it for the object-store health check; leave it alone.

Credentials and endpoint

Credentials

The operator provisions an access key for NetBox and writes it to a Secret named <cluster-name>-garage-s3, with keys access-key-id and secret-access-key. The credentials are referenced by the NetBox deployment, never copied into the NetBoxEnterprise resource.

Find your cluster name and namespace, then read the credentials:

# Find the NetBoxEnterprise resource
kubectl get netboxenterprise -A

# Read the credentials (replace <cluster-name> and <namespace>)
kubectl get secret <cluster-name>-garage-s3 -n <namespace> \
  -o jsonpath='{.data.access-key-id}' | base64 -d; echo
kubectl get secret <cluster-name>-garage-s3 -n <namespace> \
  -o jsonpath='{.data.secret-access-key}' | base64 -d; echo

The Service and the Secret share a name

<cluster-name>-garage-s3 is both the Secret holding the credentials and the Kubernetes Service serving the S3 API. They are different resource kinds, so the name collision is harmless — but specify the kind in commands to avoid confusion.

Endpoint

Garage exposes three ports:

PortAPIExposure
3900S3ClusterIP Service — not reachable from outside the cluster
3903Admin API and Prometheus metricsBearer-token auth; /health is unauthenticated
3901Inter-node RPCHeadless Service, for peering between Garage nodes only

The S3 API is deliberately cluster-internal. From inside the cluster the endpoint is:

http://<cluster-name>-garage-s3.<namespace>.svc:3900

From outside, forward the port first:

kubectl port-forward -n <namespace> svc/<cluster-name>-garage-s3 3900:3900

That makes the S3 API available at http://localhost:3900 for as long as the command runs.

The signing region is garage, not an AWS region

Garage signs with region garage. A command that falls back to an AWS default such as us-east-1 fails SigV4 signature validation, usually with an unhelpful SignatureDoesNotMatch error. Set the region explicitly on every command.

Two ways to reach the bucket

Pick whichever matches your setup. Both talk to the same bucket with the same credentials.

With the AWS CLIWith the tools NetBox Enterprise ships
Needskubectl and the aws CLI on your own machinekubectl only
Runs fromYour workstation, through a port-forwardInside the NetBox pod
Best forBulk work — migrating a whole media treeSpot checks, and anywhere you cannot install a CLI
Embedded ClusterRequires copying a kubeconfig off the applianceWorks as-is

The aws CLI is not present on an Embedded Cluster appliance

Neither the node nor netbox-enterprise shell has the aws CLI, and python3 there is the host's rather than something NetBox Enterprise manages. If you are working from the appliance, use the bundled tools.

With the AWS CLI

Start the port-forward, export the credentials you read above, and set the region explicitly:

export AWS_ACCESS_KEY_ID="<access-key-id from the Secret>"
export AWS_SECRET_ACCESS_KEY="<secret-access-key from the Secret>"
export AWS_DEFAULT_REGION="garage"

# List the top-level prefixes
aws s3 ls s3://netbox/ --endpoint-url http://localhost:3900

# List stored media
aws s3 ls s3://netbox/media/ --recursive --endpoint-url http://localhost:3900

# List stored script modules
aws s3 ls s3://netbox/scripts/ --endpoint-url http://localhost:3900

# Retrieve a copy of one object
aws s3 cp s3://netbox/media/<path> ./<path> --endpoint-url http://localhost:3900

The endpoint is plain HTTP: traffic stays inside the cluster, or inside your kubectl port-forward tunnel.

A prefix only exists while something is stored under it, so aws s3 ls s3://netbox/ shows PRE media/ alone until a script module is uploaded. Under media/ you will also see a media/cache/ tree — thumbnails NetBox generates and manages itself. Leave it alone; it rebuilds from the originals.

With the tools NetBox Enterprise ships

The NetBox pod already has boto3 installed and the endpoint, region, bucket, and credentials in its environment, so it needs no configuration and no port-forward. On Embedded Cluster, get a shell with kubectl first:

sudo /var/lib/embedded-cluster/bin/netbox-enterprise shell

Then run against the bucket from inside the pod:

POD=$(kubectl get pods -n <namespace> -l app.kubernetes.io/component=netbox \
  -o jsonpath='{.items[0].metadata.name}')

kubectl exec -n <namespace> "$POD" -c netbox -- python3 - <<'PY'
import boto3, os
s3 = boto3.client("s3",
    endpoint_url=os.environ["AWS_S3_ENDPOINT_URL"],
    region_name=os.environ["AWS_S3_REGION_NAME"])
bucket = os.environ["AWS_STORAGE_BUCKET_NAME"]

for obj in s3.list_objects_v2(Bucket=bucket).get("Contents", []):
    print(obj["Size"], obj["Key"])
PY

To upload with this path, copy the file into the pod first and then put it, since the pod is where the credentials are:

kubectl cp ./local-file.jpg -n <namespace> "$POD":/tmp/local-file.jpg -c netbox

kubectl exec -n <namespace> "$POD" -c netbox -- python3 - <<'PY'
import boto3, os
s3 = boto3.client("s3",
    endpoint_url=os.environ["AWS_S3_ENDPOINT_URL"],
    region_name=os.environ["AWS_S3_REGION_NAME"])
s3.upload_file("/tmp/local-file.jpg",
               os.environ["AWS_STORAGE_BUCKET_NAME"],
               "media/image-attachments/local-file.jpg")
PY

That two-step is why the AWS CLI path is the better one for a whole media tree — see Migrating media from another NetBox.

What you can and cannot upload

TaskDirect upload?
Migrating media whose database records came from another NetBoxYes — see Migrating media
Delivering a plugin wheelhouseYes — see Delivering a plugin wheelhouse
Replacing an existing object in placeYes, at the object's existing key
Adding a new image attachmentNo — upload it through NetBox, which creates the record
Adding a new custom scriptNo — see Adding new scripts

The distinction is whether a database record already points at the key you are writing to.

Adding new scripts

A custom script is a ScriptModule — a database row recording a file path — not simply a file in the scripts/ prefix. NetBox lists the rows it has, and nothing scans the bucket for new files. There is no rescan command, so a new .py copied into scripts/ never appears in the UI.

To add scripts, use one of these instead:

  • Upload through the UI or REST API. This writes the object and creates the record in one step. This is also how to migrate scripts from another install — see Migrating to NetBox Enterprise.
  • Sync from a Data Source. NetBox can sync scripts from a Data Source, and its backends include amazon-s3 — so you can point a Data Source at a bucket and prefix of your own and sync from it. This is the right answer for managing scripts in bulk or in version control. Note this is a Data Source you configure in NetBox, separate from the media bucket described here.

Once a script exists, replacing its object at the same key does update the code it runs. One caveat if the new file changes which classes the module defines: replacing through the REST API re-synchronizes the module's Script records from the new content, and a raw object overwrite does not — so records for classes the new file adds or removes are left stale. Use the REST API for that case, and keep raw overwrites for changes within existing classes.

Migrating media from another NetBox

When you import a database from another NetBox — a community install being converted to Enterprise, for example — the image-attachment records come with it, each recording a path. The objects themselves do not. Uploading them into the bucket at the matching keys is what makes them serve again.

Keys mirror the source install's media layout under the media/ prefix. A record whose path is image-attachments/device_42_front.jpg is served from the object key media/image-attachments/device_42_front.jpg. So preserve the relative paths exactly as they were in the source install's media root — do not flatten or rename.

This is the case the AWS CLI path is for — one command copies the tree, where the bundled-tools path would mean staging every file into the pod first. With the port-forward running and the credentials exported, copy the whole tree in one go. Check the source directory is the snapshot you mean before running it: --recursive overwrites every colliding key without prompting, and because the wrong snapshot still resolves to some object at each path, nothing below will report an error — the images will simply be wrong.

# ./media is the media root copied from the source installation
aws s3 cp ./media s3://netbox/media/ --recursive \
  --endpoint-url http://localhost:3900

Then spot-check that a record's path resolves to a real object:

aws s3 ls s3://netbox/media/image-attachments/ --endpoint-url http://localhost:3900

Confirm in the UI by opening an object that has an image attachment — the image should render rather than showing as broken.

This replaces the kubectl cp step for in-cluster storage

Migrating to NetBox Enterprise describes copying the media directory into a running pod with kubectl cp. That applies to node-local storage. With in-cluster (or external) object storage, NetBox reads media from the bucket, so a copy into the pod filesystem has no effect — upload to the bucket instead.

Scripts are not migrated this way — they need their records created, so upload them through the UI as Migrating to NetBox Enterprise describes.

Delivering a plugin wheelhouse

Custom plugins are delivered as a wheelhouse tarball, and one of the supported sources is S3 — which the in-cluster bucket can serve. The default object key is media/wheelhouse.tar.gz, so this is a deliberate hand-uploaded object rather than something NetBox writes.

It is a single file, so either access path suits it. With the AWS CLI:

aws s3 cp ./wheelhouse.tar.gz s3://netbox/media/wheelhouse.tar.gz \
  --endpoint-url http://localhost:3900

Or with the bundled tools, staging it through the pod:

kubectl cp ./wheelhouse.tar.gz -n <namespace> "$POD":/tmp/wheelhouse.tar.gz -c netbox

kubectl exec -n <namespace> "$POD" -c netbox -- python3 - <<'PY'
import boto3, os
s3 = boto3.client("s3",
    endpoint_url=os.environ["AWS_S3_ENDPOINT_URL"],
    region_name=os.environ["AWS_S3_REGION_NAME"])
s3.upload_file("/tmp/wheelhouse.tar.gz",
               os.environ["AWS_STORAGE_BUCKET_NAME"],
               "media/wheelhouse.tar.gz")
PY

The wheelhouse source is then configured in the NetBoxEnterprise resource, pointing at this bucket, the in-cluster endpoint, and the credentials Secret. Five values differ from an external-S3 setup, and the last three are the ones most often missed, because their defaults are AWS conventions that the Garage Secret does not use:

  • region: garage — not an AWS region (see the warning above).
  • endpoint — the in-cluster Service address, http://<cluster-name>-garage-s3.<namespace>.svc:3900, because pods resolve it internally.
  • credentialsSecret.name<cluster-name>-garage-s3, the Secret the operator wrote.
  • credentialsSecret.accessKeyIdaccess-key-id. This field names the key within the Secret, and it defaults to AWS_ACCESS_KEY_ID, which the Garage Secret does not contain.
  • credentialsSecret.secretAccessKeysecret-access-key, for the same reason; the default is AWS_SECRET_ACCESS_KEY.

Leave the last three at their defaults and the pods cannot authenticate, because they will look for Secret keys that are not there.

For the full procedure — building the wheelhouse, the CRD fields, and redeploying so both the web and worker deployments pick it up — see Custom Plugins.

The media-directory fallback does not apply

The simplest wheelhouse method, dropping the tarball into the pod's media directory, is a filesystem check. It cannot see an object in the bucket, so with in-cluster storage use the S3 source described here.

How media is served

This affects what you see when inspecting the bucket.

With in-cluster storage, NetBox serves media through an authenticated route rather than a presigned S3 URL. Media URLs point at /plugins/nbe-media/media/<path>, which streams the object from Garage after enforcing NetBox authentication — regardless of NetBox's global login setting.

The reason is the endpoint: the Garage S3 Service is ClusterIP, so a presigned URL pointing at it would be a dead link in a browser outside the cluster. Streaming through NetBox also means S3 credentials are never exposed to the browser.

Two consequences worth knowing:

  • Objects are private. They are stored with a private ACL and are not anonymously readable, even with the port-forward open. Every request is authenticated.
  • This applies to media only, and only in-cluster. The scripts/ backend and external-S3 deployments keep the stock behavior, where media URLs are time-limited presigned links (one hour by default).

Using a scoped access key

NetBox's key has read and write access to the whole bucket. That is the right level for the uploads on this page, but it is more than most tooling needs: if you are handing credentials to something of your own, a key scoped to just what it does is preferable. An inspection or reporting script, for instance, has no reason to be able to modify media.

Whether you can add one depends on who owns the GarageCluster resource — not on which installer you used. Check before you edit anything:

kubectl get garagecluster <cluster-name>-garage -n <namespace> \
  -o jsonpath='{.metadata.ownerReferences[*].kind}'; echo

An empty result means you applied the resource yourself and own it. NetBoxEnterprise means the operator created and owns it, which is the case on Embedded Cluster and on any Helm install that enabled in-cluster storage through storageBackend: in-cluster.

Operator-owned GarageCluster

Keys added by hand do not survive, and are not revoked either

The operator re-applies the resource on every reconcile with the key list it expects, so an entry added with kubectl edit or kubectl patch is removed from spec.accessKeys at the next reconcile.

What it does not do is undo the rest. The key the operator already issued stays in Garage with its grants intact, and the Secret it wrote stays in place — so the credential keeps working while no longer being declared anywhere. It will never be rotated or recreated, and nothing in the resource records that it exists. Treat any key you added this way as something you must revoke yourself.

There is no supported way to add a scoped key on an operator-owned cluster today, so use NetBox's own credentials — including for the uploads on this page, which are supported operations with those credentials. The constraint is which keys you write to, covered in What you can and cannot upload, not whether you may write at all.

Self-applied GarageCluster

If you applied the GarageCluster yourself — a bring-your-own-cluster deployment that does not use storageBackend: in-cluster — you own the key list and can declare additional keys.

Add an entry to the existing spec.accessKeys list. Edit your resource in place rather than applying the fragment below on its own: kubectl apply with a partial manifest replaces the whole spec, which would drop NetBox's own key and, if metadata.name does not match, provision a second three-replica Garage cluster.

# Add to spec.accessKeys on the GarageCluster you already manage.
- name: audit-readonly
  secretRef: netbox-audit-s3
  grants:
    - bucket: netbox
      permissions:
        read: true

The operator writes the issued credentials into the Secret named by secretRef, under the same access-key-id and secret-access-key keys used by NetBox's own Secret. That write is create-only — an existing consumer Secret is never rotated.

What not to do

  • Do not delete objects NetBox is using. The bucket holds live application state, and removing an object leaves a database record pointing at nothing — which surfaces as a broken image rather than a clear error. Adding and replacing objects at known keys is fine; deleting is what bites.
  • Do not use the bucket as general-purpose storage. It is inside NetBox Enterprise's backup and disaster-recovery scope, so unrelated data inflates every backup and is restored alongside NetBox.
  • Do not treat the bucket as a backup. Garage replicates across three nodes for availability, which is not protection against deletion or corruption. See Backups.
  • Do not hand NetBox's credentials around casually. The same key that writes media/ writes scripts/, and replacing an object there changes the code NetBox executes — so a leaked copy is closer to remote code execution than to image tampering. On an operator-owned cluster this is the only key you get, which makes it worth guarding rather than pasting into shared tooling.

Next steps

On this page