Goals
- Provision NetBox database + user in the shared Postgres cluster without impacting Paperless.
- Keep DB admin credentials confined to the
db-postgres namespace.
- Keep app runtime credentials confined to the app namespace (
infra-netbox).
- Support repeatable onboarding for future apps by copying a folder and changing names.
Non-goals
- No changes to the Bitnami Postgres Helm release.
- No changes to Paperless role/password/database.
- No “trust” edits to
pg_hba.conf (we already recovered; now we operate normally).
Repo Layout
Create two new GitOps apps:
argocd/
apps/
db/
netbox-db-provisioner.yml
infra/
netbox.yml
k8s/
db-provisioning/
netbox/
00-onepassworditems-db-postgres.yaml
10-netbox-db-provision-job.yaml
infra/
netbox/
00-onepassworditems-infra-netbox.yaml
10-netbox-application.yaml (or keep Argo Application in argocd/apps/infra)
(Exact placement is flexible; the key is separation by concern: db-provisioning/* vs infra/*.)
Prereqs
- 1Password Operator is already installed and working.
- A 1Password vault exists for Coachlight secrets.
coachlight_admin exists in Postgres and has superuser (or at least createdb/createrole).
1Password Items Required (in 1Password)
Create these items in 1Password (single source of truth):
A) postgres-admin (DBA identity)
Fields:
username: coachlight_admin
password: <strong password>
B) netbox-db-credentials (app identity)
Fields:
username: netbox
password: <strong password>
C) netbox-redis-credentials (optional if Redis requires auth)
Fields:
password: <strong password>
D) netbox-django-secret
Fields:
secretKey: <strong random string>
E) netbox-superuser
Fields:
password: <strong password>
(Email can stay in Helm values.)
Argo Application 1: DB Provisioning (lives with database)
File: argocd/apps/db/netbox-db-provisioner.yml
- Target namespace:
db-postgres
- Sync wave: earlier than NetBox app (e.g.
"15" if NetBox is "20")
Responsibilities
Manifest: k8s/db-provisioning/netbox/00-onepassworditems-db-postgres.yaml
Create Two OnePasswordItems in namespace db-postgres:
postgres-admin → points at 1Password item postgres-admin
netbox-db-credentials → points at 1Password item netbox-db-credentials
Naming contract:
- K8s Secret name == OnePasswordItem name
- Keys inside Secret match 1Password fields (
username, password)
Manifest: k8s/db-provisioning/netbox/10-netbox-db-provision-job.yaml
Job requirements
SQL contract (idempotent)
Run via psql with ON_ERROR_STOP=1.
Use one transaction-safe sequence:
- Create role if missing (or ensure it can login)
- Set role password to match
NETBOX_DB_PASSWORD (rotate-friendly)
- Create DB if missing with owner netbox
- Ensure privileges
Implementation detail: use DO $$ ... $$; blocks for “IF NOT EXISTS” and always run ALTER ROLE ... PASSWORD.
Example SQL logic (Copilot should implement exactly this behavior):
DO create role only if not exists
ALTER ROLE netbox WITH LOGIN PASSWORD '<from secret>';
DO create database only if not exists
ALTER DATABASE netbox OWNER TO netbox; (safe even if already)
GRANT CONNECT ON DATABASE netbox TO netbox;
Argo hook behavior
Choose ONE:
Option 1 (recommended): normal Job, idempotent
- Pros: Argo doesn’t need hook semantics; reruns only if changed
- Cons: Job object persists unless TTL used
Option 2: Argo PreSync hook Job
Pick Option 2 if you want strict ordering every sync; otherwise Option 1.
TTL
Set spec.ttlSecondsAfterFinished (e.g. 300–3600) to keep namespace clean.
Argo Application 2: NetBox (lives with the app)
File: argocd/apps/infra/netbox.yml
Manifest: k8s/infra/netbox/00-onepassworditems-infra-netbox.yaml
Create OnePasswordItems (namespace infra-netbox) pointing at the same 1Password items:
netbox-db-credentials (same itemPath as db-postgres version)
netbox-redis-credentials
netbox-django-secret
netbox-superuser
This is deliberate duplication due to namespace boundaries.
NetBox Helm values wiring (in your Argo Application)
Update your existing NetBox Application values to use existingSecret* everywhere possible:
External DB (already good)
externalDatabase.existingSecretName: netbox-db-credentials
externalDatabase.existingSecretKey: password
externalDatabase.username: netbox (or omit if chart reads from secret; follow chart schema)
Redis
externalRedis.existingSecretName: netbox-redis-credentials
externalRedis.existingSecretKey: password
Django secret key + superuser password
- Configure the chart to source these from Kubernetes Secrets created by OnePasswordItem
- Use the chart’s supported
existingSecret / secretKey references (Copilot must read chart values/schema and wire correctly; no guessing).
Ordering / Sync Waves
Security / Blast Radius Rules
-
DB admin Secret (postgres-admin) exists only in db-postgres.
-
App namespaces never receive DB admin creds.
-
DB provision jobs use least exposure:
- only connect to Postgres service within cluster
- no broad RBAC beyond creating Jobs/reading Secrets in their namespace
-
App DB user (netbox) is not superuser.
Rotation Workflow
-
Rotate password in 1Password item netbox-db-credentials.
-
Both namespace Secrets update automatically via 1Password operator.
-
Re-sync Argo:
- DB provision Job runs and executes
ALTER ROLE netbox PASSWORD ...
- NetBox deployment restarts if needed (depends on chart; ideally it watches secret or you trigger a rollout)
Do the same for postgres-admin only when needed.
Acceptance Criteria
-
infra-netbox namespace contains K8s Secrets:
netbox-db-credentials
netbox-redis-credentials
netbox-django-secret
netbox-superuser
-
db-postgres namespace contains:
postgres-admin Secret
netbox-db-credentials Secret
netbox-db-provision Job succeeded (or hook ran successfully)
-
In Postgres:
- database
netbox exists
- role
netbox exists and can login
- owner/grants correct
-
Paperless still works:
PGPASSWORD="$(cat "$POSTGRES_PASSWORD_FILE")" psql -U paperless -d paperless -c "select 1;" returns 1
-
NetBox pods start successfully and connect to Postgres and Redis.
Implementation Notes for Copilot
- Do not invent NetBox chart secret field names. Read chart docs/values and wire secrets correctly.
- Keep SQL idempotent and safe. Never drop/alter Paperless DB/role.
- Ensure the Job fails fast with
-v ON_ERROR_STOP=1.
- Use
args/command that are robust and don’t echo secrets.
- Prefer
psql variables or env injection; avoid printing passwords.
Goals
db-postgresnamespace.infra-netbox).Non-goals
pg_hba.conf(we already recovered; now we operate normally).Repo Layout
Create two new GitOps apps:
(Exact placement is flexible; the key is separation by concern:
db-provisioning/*vsinfra/*.)Prereqs
coachlight_adminexists in Postgres and has superuser (or at least createdb/createrole).1Password Items Required (in 1Password)
Create these items in 1Password (single source of truth):
A)
postgres-admin(DBA identity)Fields:
username:coachlight_adminpassword:<strong password>B)
netbox-db-credentials(app identity)Fields:
username:netboxpassword:<strong password>C)
netbox-redis-credentials(optional if Redis requires auth)Fields:
password:<strong password>D)
netbox-django-secretFields:
secretKey:<strong random string>E)
netbox-superuserFields:
password:<strong password>(Email can stay in Helm values.)
Argo Application 1: DB Provisioning (lives with database)
File:
argocd/apps/db/netbox-db-provisioner.ymldb-postgres"15"if NetBox is"20")Responsibilities
Create secrets in
db-postgresusing OnePasswordItem:postgres-adminnetbox-db-credentialsRun a Job that:
netboxif missingnetboxif missingManifest:
k8s/db-provisioning/netbox/00-onepassworditems-db-postgres.yamlCreate Two OnePasswordItems in namespace
db-postgres:postgres-admin→ points at 1Password itempostgres-adminnetbox-db-credentials→ points at 1Password itemnetbox-db-credentialsNaming contract:
username,password)Manifest:
k8s/db-provisioning/netbox/10-netbox-db-provision-job.yamlJob requirements
Namespace:
db-postgresUses a container with
psqlavailable (Bitnami postgres image is fine).Reads env from Secrets:
POSTGRES_ADMIN_USER/POSTGRES_ADMIN_PASSWORDfrompostgres-adminsecret keysusername/passwordNETBOX_DB_USER/NETBOX_DB_PASSWORDfromnetbox-db-credentialssecret keysusername/passwordConnects to the Postgres service:
postgres-postgresql.db-postgres.svc.cluster.local5432postgres(maintenance DB)Runs idempotent SQL.
SQL contract (idempotent)
Run via
psqlwithON_ERROR_STOP=1.Use one transaction-safe sequence:
NETBOX_DB_PASSWORD(rotate-friendly)Implementation detail: use
DO $$ ... $$;blocks for “IF NOT EXISTS” and always runALTER ROLE ... PASSWORD.Example SQL logic (Copilot should implement exactly this behavior):
DOcreate role only if not existsALTER ROLE netbox WITH LOGIN PASSWORD '<from secret>';DOcreate database only if not existsALTER DATABASE netbox OWNER TO netbox;(safe even if already)GRANT CONNECT ON DATABASE netbox TO netbox;Argo hook behavior
Choose ONE:
Option 1 (recommended): normal Job, idempotent
Option 2: Argo PreSync hook Job
Add annotations:
argocd.argoproj.io/hook: PreSyncargocd.argoproj.io/hook-delete-policy: HookSucceededPros: runs automatically before sync and cleans up
Cons: reruns more often; still fine because SQL is idempotent
Pick Option 2 if you want strict ordering every sync; otherwise Option 1.
TTL
Set
spec.ttlSecondsAfterFinished(e.g. 300–3600) to keep namespace clean.Argo Application 2: NetBox (lives with the app)
File:
argocd/apps/infra/netbox.ymlDestination namespace:
infra-netboxSync wave:
"20"(after db provisioner)Contains:
infra-netboxfor runtime secretsManifest:
k8s/infra/netbox/00-onepassworditems-infra-netbox.yamlCreate OnePasswordItems (namespace
infra-netbox) pointing at the same 1Password items:netbox-db-credentials(same itemPath as db-postgres version)netbox-redis-credentialsnetbox-django-secretnetbox-superuserThis is deliberate duplication due to namespace boundaries.
NetBox Helm values wiring (in your Argo Application)
Update your existing NetBox Application values to use
existingSecret*everywhere possible:External DB (already good)
externalDatabase.existingSecretName: netbox-db-credentialsexternalDatabase.existingSecretKey: passwordexternalDatabase.username: netbox(or omit if chart reads from secret; follow chart schema)Redis
externalRedis.existingSecretName: netbox-redis-credentialsexternalRedis.existingSecretKey: passwordDjango secret key + superuser password
existingSecret/secretKeyreferences (Copilot must read chart values/schema and wire correctly; no guessing).Ordering / Sync Waves
DB provisioner app: sync-wave "15"
NetBox app: sync-wave "20"
Within each app:
00-and10-, or by Argo wave annotations if needed)Security / Blast Radius Rules
DB admin Secret (
postgres-admin) exists only indb-postgres.App namespaces never receive DB admin creds.
DB provision jobs use least exposure:
App DB user (
netbox) is not superuser.Rotation Workflow
Rotate password in 1Password item
netbox-db-credentials.Both namespace Secrets update automatically via 1Password operator.
Re-sync Argo:
ALTER ROLE netbox PASSWORD ...Do the same for
postgres-adminonly when needed.Acceptance Criteria
infra-netboxnamespace contains K8s Secrets:netbox-db-credentialsnetbox-redis-credentialsnetbox-django-secretnetbox-superuserdb-postgresnamespace contains:postgres-adminSecretnetbox-db-credentialsSecretnetbox-db-provisionJob succeeded (or hook ran successfully)In Postgres:
netboxexistsnetboxexists and can loginPaperless still works:
PGPASSWORD="$(cat "$POSTGRES_PASSWORD_FILE")" psql -U paperless -d paperless -c "select 1;"returns 1NetBox pods start successfully and connect to Postgres and Redis.
Implementation Notes for Copilot
-v ON_ERROR_STOP=1.args/commandthat are robust and don’t echo secrets.psqlvariables or env injection; avoid printing passwords.