Skip to main content

Credentials

Overview​

Credentials store secrets that collector plugins need to connect to monitored services (databases, web servers, container runtimes, etc.). Every credential is:

  • Encrypted at rest via HashiCorp Vault's Transit secrets engine
  • Client-scoped — credentials belong to a client and optionally an environment
  • RBAC-controlled — access requires explicit credentials:* permissions
  • Type-aware — each credential type defines required, optional, and secret fields

Agents never see raw credentials in their configuration. When a host config references a credential_id, the backend's credential resolver decrypts the secret fields at config-push time and injects them into the collector parameters sent over NATS.

Architecture​

The Provider Registry is a pluggable abstraction. The current implementation uses Vault Transit for envelope encryption, but the Provider interface supports alternative KMS backends:

type Provider interface {
Get(ctx context.Context, in SecretRef) (map[string]string, error)
Set(ctx context.Context, ref string, data map[string]string) (SecretRef, error)
Delete(ctx context.Context, ref string) error
Type() string
}

Credential Types​

Each credential type defines which fields are allowed, which are required, and which contain secrets (redacted in API responses).

TypeRequired FieldsSecret FieldsOptional Fields
postgresqldsndsn--
redisaddrpassworddb
nginxurlpasswordusername
dockersocket_path or registry_urlregistry_passwordregistry_user
generic--all fieldsany key-value pair
info

The generic type accepts any key-value pairs with no field restrictions. All fields are treated as secret and redacted in API responses.

Encryption​

Credential data is encrypted using Vault's Transit secrets engine with the key named proxima-creds. This is the default and what production runs.

A deployment with PROXIMA_KEYSTORE=disk uses AES-256-GCM under a 32-byte key-encryption key read from the keys volume instead (see Disk Keystore). The envelope shape is the same — the map is JSON-marshaled, sealed whole, and the ciphertext travels back to sit on the credential row — and the provider registers under the same name, local, because that name records that the ciphertext lives on the row rather than behind a reference in an external manager. The two are never registered together: the boot switch picks exactly one secret backend.

Encryption flow:

  1. Credential data (a map[string]string) is JSON-marshaled
  2. The JSON bytes are base64-encoded
  3. Vault Transit encrypts the encoded payload, returning a ciphertext string (vault:v1:...)
  4. The ciphertext is stored in the credentials table alongside the credential metadata

Decryption flow (on read or credential resolution):

  1. The ciphertext is sent to Vault Transit for decryption
  2. Vault returns the base64-encoded plaintext
  3. The plaintext is decoded and unmarshaled back to the original key-value map
warning

On the Vault path (the default): Vault must be unsealed and reachable for any credential operation. If Vault is unavailable, credential create, read, update, and resolution all fail. The backend logs these failures and returns 500 Internal Server Error. A PROXIMA_KEYSTORE=disk deployment has no such dependency — its equivalent single point of failure is the KEK file on the keys volume, which is read once at boot.

Switching keystores strands existing credentials

Transit ciphertexts begin vault:v1:; the local encryptor writes local:v1: and cannot read the other. A deployment that moves from PROXIMA_KEYSTORE=vault to disk cannot decrypt its existing credential rows — nor its webhook signing secrets, alert-source tokens or pull-source credentials, all of which are sealed by the same encryptor. Where the prefix actually shows up matters, because it is not everywhere. The SecretEncryptor surfaces pass the error through: a pull source's last_error reads decrypt credentials: credential: ciphertext is not local:v1-encrypted, and the SSH CA names it in the boot failure. The credential surfaces do not. Tester.Test replaces it with failed to decrypt credential data, and the agent's credential resolver with decryption failed — on both, the prefix survives only inside an OpenTelemetry span. What an operator sees is worse than the substitute, because it takes work to see anything at all. The edit dialog's connection test is automatic but gated: it clears the form's secret fields on open and stays idle — rendering no result row whatsoever — until every required field is non-empty. For postgresql (dsn), redis (addr) and nginx (url) that means a blank panel on open; the operator has to retype the secret and wait out an 800 ms debounce before failed to decrypt credential data appears. Only docker and generic, which have no required fields, test on open. And that message never names the prefix — it is in Tempo, not in the response. No migration path exists, deliberately: re-entering the affected credentials is the only route. Choose the keystore when the deployment is created.

RBAC​

Credential endpoints require the following permissions, scoped to the client that owns the credential:

PermissionRequired For
credentials:readList credentials, get credential details
credentials:writeCreate credentials, update credentials, test credentials
credentials:deleteDelete credentials

Super admins bypass all permission checks.

Service Accounts​

Service accounts can create, list, and manage credentials via API key authentication (X-API-Key header). When a service account creates or updates a credential, the created_by and updated_by fields are set to null (since service account subjects reference the service_accounts table, not the users table referenced by the FK constraint).

API Endpoints​

All credential endpoints are nested under a client:

MethodPathDescription
POST/api/v1/clients/{id}/credentialsCreate a credential
GET/api/v1/clients/{id}/credentialsList credentials for a client
GET/api/v1/clients/{id}/credentials/{credentialID}Get a credential
PUT/api/v1/clients/{id}/credentials/{credentialID}Update a credential
DELETE/api/v1/clients/{id}/credentials/{credentialID}Delete a credential
POST/api/v1/clients/{id}/credentials/{credentialID}/testTest a credential
tip

Secret fields are redacted in GET responses (replaced with "***"). The raw values are never returned after creation.

Credential Lifecycle​

1. Create​

Create a credential with a name, type, and data fields:

POST /api/v1/clients/{clientID}/credentials
{
"name": "Production DB",
"credential_type": "postgresql",
"provider": "local",
"environment_id": "env-uuid",
"data": {
"dsn": "postgres://user:pass@db-host:5432/mydb"
}
}

The data is validated against the credential type's field definitions, encrypted via Vault Transit, and stored in PostgreSQL.

2. Use in Collector Config​

Reference the credential in a host's collector configuration by its credential_id. When the backend pushes config to the agent, the Credential Resolver decrypts the secret fields and injects them into the collector parameters.

3. Rotate​

Update the credential's data to rotate secrets:

PUT /api/v1/clients/{clientID}/credentials/{credentialID}
{
"data": {
"dsn": "postgres://user:new-pass@db-host:5432/mydb"
}
}

4. Delete​

Remove a credential when it is no longer needed. Deletion is blocked if any host configs reference it.

Rotation​

When a credential's data is updated:

  1. The new data is validated and encrypted
  2. last_rotated_at is set to the current timestamp
  3. revision is incremented
  4. Affected hosts (those with collector configs referencing this credential) receive a config re-push via NATS

This ensures agents pick up rotated secrets without manual intervention or restarts.

Reference Guarding​

Credentials cannot be deleted while host configurations reference them. A DELETE request for a referenced credential returns:

Response: 409 Conflict

{
"error": {
"code": "conflict",
"message": "credential is referenced by 2 host(s)",
"request_id": "req_abc123"
}
}

The response includes the host_ids that reference the credential, so operators can update or remove those references before retrying the delete.

warning

Always check for host config references before attempting to delete a credential. Use the error response to identify which hosts need updating.

Connectivity Testing​

POST /api/v1/clients/{clientID}/credentials/{credentialID}/test

The test endpoint validates credential fields against the type definition (required fields present, no unknown fields for typed credentials).

info

Testing currently performs field validation only -- it does not attempt a live connection to the target service. Live connectivity testing is planned for a future release.

Least-Privilege Setup​

Each credential should grant the minimum permissions the agent needs to collect metrics. Below are practical setup instructions for each credential type.

PostgreSQL​

-- Create a dedicated monitoring user (do NOT use postgres/root/admin)
CREATE USER proxima_mon WITH PASSWORD 'your_secure_password';

-- Grant the pg_monitor role (read-only access to pg_stat_* views)
GRANT pg_monitor TO proxima_mon;

-- Grant CONNECT on each database you want to monitor
GRANT CONNECT ON DATABASE mydb TO proxima_mon;

Why this works:

  • pg_monitor is a built-in PostgreSQL role that bundles pg_read_all_stats, pg_read_all_settings, and pg_stat_scan_tables. It provides read-only access to all the system views the agent queries.
  • The agent only runs SELECT queries on system views -- no write access is needed.
  • Never use a superuser (postgres, root, admin) for monitoring. If the credential is compromised, the blast radius should be limited to read-only stats access.

Redis​

ACL SETUSER proxima on >your_password ~* +info +config|get +client|list +dbsize +slowlog|get

This creates a Redis 6+ ACL user with the minimum permissions the agent needs:

PermissionPurpose
+infoServer info and stats
+config|getRead configuration values (no SET)
+client|listList connected clients
+dbsizeDatabase key count
+slowlog|getRead the slow query log
note

For Redis < 6 (no ACL support), use requirepass and ensure the server is not exposed to untrusted networks. There is no way to restrict commands without ACL, so network-level isolation is critical.

Nginx​

# Enable stub_status on a restricted endpoint
location /stub_status {
stub_status;
allow 127.0.0.1; # Only localhost
allow 10.0.0.0/8; # Or your internal network
deny all;
}

Key points:

  • The stub_status module provides basic connection and request metrics. It is compiled into most Nginx builds by default.
  • Restrict access to localhost or internal networks only -- do not expose this endpoint publicly.
  • No authentication is needed if the endpoint is properly firewalled.
  • If basic auth is required (e.g., the agent runs on a different host), create a dedicated read-only user with htpasswd and reference the username/password in the Nginx credential.

Docker​

# Read-only socket mount (recommended)
-v /var/run/docker.sock:/var/run/docker.sock:ro

# Or add the agent to the docker group
usermod -aG docker proxima-agent

Key points:

  • Mount the Docker socket as read-only (:ro) when possible. The agent only reads container metadata and stats.
  • For container registries, use a token or service account with pull-only access. Never grant push or admin permissions to a monitoring credential.
  • Never expose the Docker socket over TCP without TLS. An unencrypted, network-accessible Docker socket is equivalent to root access on the host.

Generic​

Generic credentials are custom key-value pairs where all values are treated as secrets. Use these for integrations not covered by the built-in types.

When creating generic credentials, follow the principle of least privilege for whatever service you are connecting to:

  • Create a dedicated service account or API token for monitoring.
  • Grant only the read or stats permissions the agent needs.
  • Avoid reusing credentials that have write or admin access.
  • Rotate the credential on a regular schedule using the rotation API.

Management UI​

Credentials can be managed through the Console frontend in two places:

  • Client Detail → Credentials Tab — create, edit, and delete credentials scoped to a single client. The form dialog dynamically renders required/optional/secret fields based on the selected credential type and shows security warnings (e.g., least-privilege setup links). After creation, the credential is automatically tested via the /test endpoint.
  • Admin → Credentials Page (/admin/credentials) — cross-client view for administrators. Lists credentials across all accessible clients with search filtering.

See the Frontend Overview for full component details.