-
Notifications
You must be signed in to change notification settings - Fork 13
ADR: Centralize secret storage and harden admin credentials
Date: 2026-07-24
Related issue: https://github.com/device-management-toolkit/console/issues/1067
High. This decision provides a supported, safer non-OAuth2 credential workflow, reduces plaintext credential exposure, and prevents a shared or predefined JWT signing key from being deployed accidentally.
Issue #1067 requires secure credential storage and a customer-usable workflow for Console deployments that do not use OAuth2. Before this change, standalone deployments can rely on plaintext admin values in configuration, and credential handling is split across startup and login paths.
Key problems:
- plaintext credentials can remain in
config.yml - no first-class workflow exists to add, remove, inspect, or change the standalone administrator credentials
- secret logic is not centralized
- a predefined JWT key is unsafe, and JWT-key lifecycle differs between a single instance and replicated deployments
This ADR covers standalone CLI credential management and secure local storage. First-login UI, password-management UI, and external identity-provider work are deferred to separate ADRs. It is a target design: the current Console code already uses github.com/zalando/go-keyring for the local encryption key with service name device-management-toolkit; the dmt-console admin-secret store and the new CLI options described below extend that pattern.
OAuth2/OIDC takes precedence in every deployment topology, including standalone. When AUTH_CLIENT_ID is non-empty, Console uses the OAuth2/OIDC path and does not load or use standalone admin credentials from the keyring, .env, environment, or config.yml. This makes an OAuth2 deployment deterministic and prevents a standalone credential source from silently overriding its configured identity provider.
This workflow applies only when AUTH_CLIENT_ID is empty. During standalone Console startup, when no administrator credential is available from a supported source, Console interactively prompts for an administrator login ID and password without echoing the password, then stores the login ID and bcrypt password hash. Automated deployments provide credentials through the documented source hierarchy before starting Console.
Supported commands:
-
console --remove-admin- remove the stored standalone administrator credentials after confirmation -
console --change-admin-password- replace the administrator password after identity/authorization checks appropriate to the CLI context -
console --show-admin- show the login ID only; never show a password or password hash
Admin credentials use a dedicated keyring service name, dmt-console. The password is stored only as a bcrypt hash. The existing encryption-key service (device-management-toolkit) remains unchanged and is not reused for administrator credentials.
| Operating system | Keyring backend through github.com/zalando/go-keyring
|
Operational consideration |
|---|---|---|
| Windows | Windows Credential Manager | Operators can view or remove entries with Credential Manager. |
| macOS | Keychain | Operators can view or remove entries with Keychain Access. |
| Linux | Secret Service API, typically GNOME Keyring or KWallet | A Secret Service provider and an unlocked user session are required; no file-based keyring fallback is enabled by default. |
The package is cross-platform, but the OS keyring is user and session scoped. It is therefore suitable for an interactive standalone installation, not as the shared source of truth for replicas or headless cloud workloads.
Diagram: Standalone credential resolution and migration
flowchart TD
A[Console startup] --> B{AUTH_CLIENT_ID configured}
B -->|Yes| C[Start OAuth2 OIDC mode]
B -->|No| D{Keyring available}
D -->|Yes| E[Read username and password hash from keyring]
E --> F{Credentials found}
F -->|Yes| G[Start standalone login]
F -->|No| H[Resolve env file environment and config]
D -->|No| H
H --> I{Credentials found}
I -->|No| J[Prompt for login ID and password]
I -->|Yes| K{Credentials came from config}
K -->|No| G
K -->|Yes| L[Show migration summary without secret values]
L --> M{Operator confirms migration}
M -->|Yes| N{Keyring available for write}
N -->|Yes| O[Store username and password hash in keyring]
O --> P[Scrub admin credentials from config]
P --> G
N -->|No| Q[Keep config values and show remediation]
M -->|No| R[Keep config values and show warning]
J --> S[Create bcrypt password hash]
S --> N
For standalone credential resolution, the source priority is:
-
dmt-consoleOS keyring -
.envvalues - environment variables
config.yml
When Console startup cannot resolve credentials from these sources, it prompts the interactive operator to enroll an administrator and stores the login ID and bcrypt password hash. It does not prompt in a non-interactive session; that session fails with an actionable error instead of writing plaintext credentials.
Config migration confirmation: when credentials are resolved from config.yml, Console shows a migration summary before making any change. The summary identifies auth.adminUsername and auth.adminPassword as the fields to be moved to the dmt-console keyring and then removed from config.yml; it never prints the password or bcrypt hash. The operator must explicitly confirm. Only after confirmation and a successful keyring write does Console scrub those two fields. auth.jwtKey is governed separately by the JWT-key policy below and is not moved to the dmt-console keyring. Other configuration values, including auth.clientId, auth.issuer, and UI OAuth settings, are not scrubbed because they are not administrator credentials.
Keyring unavailable fallback: when the keyring is missing, locked, access is denied, or no supported backend is available, Console first resolves existing credentials from .env, environment variables, then config.yml. For a locked or denied keyring, Console displays: Unable to access the OS keyring. Unlock the keyring or sign in to the desktop session, verify this user has access, then retry. The message includes the underlying error without exposing credentials. Console does not move or erase config.yml values while keyring access fails. For a new or changed credential, the operator can unlock the keyring/session, grant process access, or configure a shared secret store for cloud deployments. It never silently writes plaintext. An interactive operator may explicitly confirm a plaintext config.yml fallback, which logs a warning on every startup while the values remain. A non-interactive run fails with an actionable error instead of writing plaintext.
Password hashing is an additional security control beyond the explicit wording of issue #1067. bcrypt protects stored passwords if a keyring export, backup, or configuration fallback is disclosed, since the original password cannot be recovered from the hash. It also supports slow, salted offline-verification resistance. The tradeoffs are that bcrypt hashes cannot be displayed or recovered, verification consumes deliberate CPU time, and a hash does not prevent online guessing; rate limiting and audit logging remain necessary.
Security note: an actor with the same OS credential-store access as Console can replace stored values. This risk is inherent to that access level and must be controlled through OS account and keyring permissions.
Diagram: Standalone password verification
flowchart TD
A[Standalone login submission] --> B[Load password hash from runtime credential store]
B --> C{bcrypt comparison succeeds?}
C -->|Yes| D[Issue authenticated session]
C -->|No| E[Reject login and record failed attempt]
F[Plaintext password] --> G[bcrypt hash during add or change]
G --> H[Persist hash only]
Console must never ship or accept a known, predefined AUTH_JWT_KEY such as a sample/default value. IN_DOCKER_RUN is a proposed explicit deployment-mode environment variable passed by Docker Compose or other deployment tooling. It distinguishes a local standalone process from a managed deployment; it must not itself contain a secret and must not be used as a security boundary.
- Standalone (
IN_DOCKER_RUNunset or false): generate a new cryptographically random JWT signing key in memory at each Console start. All sessions are invalidated on restart. The key is never written toconfig.yml, the OS keyring, logs, or environment. - Docker Compose (
IN_DOCKER_RUN=true): a one-time pre-init job generates a random key when absent and stores it in a restricted persistent secret file or Docker secret. Console and Kong receive the same key through their runtime secret mounts. Recreating a Console container does not invalidate sessions while the secret persists. - Kubernetes/cloud/enterprise (
IN_DOCKER_RUN=trueplus the platform deployment configuration): use a KubernetesSecretcreated or rotated by a well-defined initialization mechanism, such as a Helm hook/job or an external-secret controller. Mount or inject the identical key into every Console replica and Kong. Do not generate keys independently in application pods.
Diagram: JWT key lifecycle by deployment topology
flowchart TD
A[Deployment starts] --> B{Standalone?}
B -->|Yes| C[Generate random JWT key in memory]
C --> D[Start one Console instance]
B -->|No| E{Docker Compose?}
E -->|Yes| F[Pre-init creates random key once]
F --> G[Persist as restricted Compose secret]
G --> H[Mount same key in Console and Kong]
E -->|No| I[Kubernetes or cloud]
I --> J[Helm init job or external-secret controller]
J --> K[Create or rotate Kubernetes Secret]
K --> L[Inject same key into all replicas and Kong]
Diagram: Shared JWT key distribution and validation
sequenceDiagram
autonumber
participant Init as Compose pre-init / K8s init job
participant Secret as Secret storage
participant Console as Console replicas
participant Kong as Kong
participant User
Init->>Secret: Read JWT key
alt Key absent
Init->>Init: Generate cryptographically random key
Init->>Secret: Store with restricted access
end
Secret-->>Console: Inject same JWT key
Secret-->>Kong: Inject same JWT key
User->>Console: Authenticate
Console->>Console: Sign JWT
User->>Kong: Call protected API with JWT
Kong->>Kong: Validate with same JWT key
Key rotation must be an explicit maintenance operation. For the initial implementation, rotating the shared key invalidates active sessions and rolls Console and Kong together. A later enhancement can support overlapping key IDs to permit gradual rotation.
First-login and password-management UI, along with external identity-provider and registration behavior, are out of scope for this ADR. They require separate ADRs to establish their user journeys, recovery controls, authorization model, and deployment policy.
- Console automatically migrates plaintext administrator values from an existing standalone installation to the keyring, then scrubs them from
config.ymlafter a successful write. If the keyring is unavailable, plaintext fallback remains only where an operator explicitly accepts its risk. - Local keyring entries are tied to the OS user/session, so headless and replicated deployments require the deployment platform's shared secret mechanism.
- A standalone restart invalidates sessions by design; shared deployment key rotation invalidates sessions until a future key-ID rotation design is available.
- Password recovery depends on configured email or SMS delivery, abuse controls, and privacy approval. Federated providers also depend on product policy and configured OIDC clients.
- Requirement 1 (secure storage/retrieval without OAuth2): this ADR adds keyring-first standalone credential storage and retrieval.
- Requirement 2 (disable plaintext in config): successful keyring writes scrub
auth.adminUsernameandauth.adminPassword; plaintext fallback requires confirmation and warning. - Requirement 3 (set/update/remove workflow): Console startup supplies interactive standalone enrollment when no credential source is available; the CLI also supplies remove, password-change, and show-login-ID workflows.
- Requirement 4 (supported environments): this ADR uses the existing cross-platform
zalando/go-keyringintegration pattern; shared deployments use their deployment platform's secret store.
- For the OS key ring please mention that what mechanism you would be using across different OSes. For example, on Windows you would be using the Windows Credential Manager, on MacOS you would be using the Keychain, and on Linux you would be using the Secret Service API (or a fallback like a file-based keyring if the Secret Service API is not available). This ensures that users understand how their credentials are being stored securely across different platforms.
- Password handling as bcrypt is needed requirement w.r.t security but the issue https://github.com/device-management-toolkit/console/issues/1067 does not explicitly mention it. It would be good to clarify that this is an additional security measure being implemented to enhance the protection of admin credentials.
- When using OS key ring please capture the go package that would be used for keyring integration. The well known one is https://github.com/zalando/go-keyring or are there any other packages that you are considering? This will help in understanding the implementation details and any potential limitations or considerations with the chosen package.
- Mention the service-name that would be used for storing the credentials in the OS key ring (ex: dmt-console). This is important for users to know how to identify and manage their stored credentials within the keyring.
- Alternatives to this requirement "Provide a customer-usable workflow for setting, updating, and removing stored credentials. This will need some brainstorming" are not captured. In the current ADR you are proposing additional flags to console for the workflow. Have your consider the utilities provided by OS to manage these credentials? For example, on Windows, users can manage their credentials through the Credential Manager, and on MacOS, they can use the Keychain Access application. It would be good to mention these alternatives and how they compare to the proposed workflow in terms of usability and security.
- In the current decision logic you always take the credentials from keycloak if they are present and ignore the environment variables. Its good to clarify and capture this expectation.
- Capture which configurations from config.yaml are moved into the keyring and also scrubbed after successful keyring writes. This will help users understand which sensitive information is being removed from the configuration file to enhance security.
- Capture whether you would print a warning or a get a confirmation from the user when the credentials are stored in plain text in config.yaml.
- Are they scenarios to be handled when the keyring is locked/permission is denied and need to put a message to user to unlock the keyring? This is important to ensure that users are aware of any issues with accessing their stored credentials and can take appropriate action.