Key Vault content standard
Standard scope
Applies to: Every secret, key, and certificate in every HCS-managed or HCS-accessed Key Vault, across all scopes Domain: keyvault Status: Active
This standard governs everything stored inside Azure Key Vault — secrets, keys, and certificates. It is not about the KV resource itself; see naming for resource naming (kv-<org>-<env>-<region>-<instance>).
This document is the authoritative multi-vault model for HCS. Nothing in this standard assumes a single vault — every rule below (naming, access control, the keyvault:// URI scheme) is written to work across any number of vaults owned by any number of organizations. Where kv-hcs-vault-01 appears as a default or an example, that default applies only within the hcs scope; other scopes (TierPoint, customers) use their own vault, resolved from config, never assumed. See C3 in the consolidation matrix for the reasoning.
Every object stored in any HCS-managed Key Vault must conform to this standard. No exceptions.
Scope
This standard applies to:
- All secrets, keys, and certificates in
kv-hcs-vault-01and any future HCS-managed vault - All organisations — HCS, TierPoint, Country Cloud Boy, and any customer-managed vault
- All automation that creates, reads, rotates, or deletes vault objects
What belongs in Key Vault
Store in KV:
- Passwords and credentials for any service, account, or workload
- API keys, PATs, and OAuth tokens
- Service principal client secrets
- Managed identity client IDs (for reference by scripts and pipelines)
- Certificate private keys
- Connection strings
- Any value whose exposure would require a security response
Do not store in KV:
- Account usernames or display names that have no associated secret (store those in config YAML)
- Public endpoints, URLs, or region identifiers
- Non-secret configuration — cluster names, subscription IDs, tenant IDs
- Secrets fully managed by a purpose-built store with no other consumers
Object types
| Type | Use for | CLI noun |
|---|---|---|
| Secret | Passwords, tokens, keys, connection strings, client secrets | secret |
| Key | Cryptographic operations — disk encryption, backup encryption, key wrapping | key |
| Certificate | TLS/SSL certificates with automated renewal and private key management | certificate |
Naming standard
The pattern
<org>-<env-name>-<descriptor>-<field>| Segment | Purpose | Rules |
|---|---|---|
org | Who owns it | 2–4 char lowercase identifier, unique across all vaults — always first |
env-name | Which specific environment | Short memorable name — never a generic type like prd or poc — see env-name rules below |
descriptor | What this secret is for | Account type, service, or resource — be specific: domain-admin, arc-registration-spn, azl-cl01-lcm-01 |
field | What value the secret holds | password, username, secret, client-id, pat, api-key, api-token, conn-str, private-key |
Rules
lowercase-kebab-case— no underscores, no camelCase, no PascalCase- No region segment — region is a deployment detail, not an identifier
- No generic environment type (
prd,poc,dev) as the env-name — use the specific name of the environment - Usernames and passwords for the same account are separate secrets with matching names that differ only in the final
<field>segment - Never include the vault name in the object name
- Service principals: purpose-first —
arc-registration-spn, notspn-arc-registration - Managed identities: purpose-first —
arc-management-mi, notmi-arc-management - Account identifiers for human logins are derived from the full UPN/username, kebab-cased, with
@and.replaced by-. Never abbreviate:kris-hybridsolutions-cloudis correct;kjt,kris, orhcsare not. - Every password secret must have a paired username secret with an identical name differing only in the
<field>segment. Existing passwords without a matching username must have one created at the next rotation. - The env-name must appear in the env-name registry table. No ad-hoc env-names.
Approved field values
The <field> segment must be one of the following. Do not invent new values.
| Field value | Use for |
|---|---|
username | Login name or UPN value |
password | Password |
pat | Personal Access Token (GitHub, GitLab, ADO) |
app-id | GitHub App ID |
app-private-key | GitHub App private key |
client-id | Service principal or managed identity client ID |
secret | Service principal client secret |
api-key | SaaS API key (Anthropic, OpenAI, etc.) |
api-token | API token distinct from a PAT (e.g. Cloudflare) |
conn-str | Connection string |
private-key | TLS or certificate private key (PEM or base64) |
recovery-code | Entra MFA or account recovery code |
deploy-token | Static Web Apps or static host deploy token |
Org identifier rules
- 2–4 characters, lowercase, no hyphens
- Must be short enough to be instantly recognisable
- Must be unique across all vaults — agree the identifier before using it for the first time
Environment name rules
- Short and memorable — someone who works in that environment must recognise it immediately
- Names the specific environment, not its type —
tplabsnottppoc,hcsprodnothcsprd - For shared platform services with no infrastructure target (PATs, API keys, SaaS tokens), use
platformas the env-name
Env-name registry
Every env-name in use must appear in this table. An env-name not listed here is not valid. Adding a new env-name requires a PR and an ADO work item that updates this table.
| env-name | Meaning | Examples of secrets that belong here |
|---|---|---|
platform | HCS-wide automation and shared service credentials. No human logins. | Master GitHub PAT, ADO platform PAT, Anthropic API key, Cloudflare token, SWA deploy token |
personal | Admin credentials tied to a human identity. Always tagged lifecycle=temporary. Never use platform for a human credential. | Kris's Entra logins, GitHub account password, MFA recovery codes |
hcsprod | HCS production workload environment (infrastructure, clusters, services). | HCS production cluster admin passwords, domain join credentials |
tplabs | TierPoint lab environment workload secrets. | Azure Local cluster LCM accounts, domain admin, deployment accounts |
tppoc | TierPoint customer proof-of-concept environment. | TierPoint PoC workload secrets |
tpdemos | TierPoint internal demo environment. | TierPoint demo workload secrets |
blog-thisismydemo | thisismydemo.cloud blog infrastructure secrets. | Blog deploy tokens, GitHub repo deploy keys |
phx | Project Phoenix environment (projectphx.cloud). | Phoenix cluster credentials, platform secrets |
<repo-name> | Per-repo scoped credentials. env-name matches the repo name exactly — e.g. azurelocal-ranger, azure-scout, azurelocal-surveyor. | Per-repo PATs, per-repo deploy tokens |
<org-name> | Per-org scoped credentials when repo scope is not enough. env-name matches the GitHub/GitLab org name — e.g. azurelocal, thisismydemo, hybridcloudsolutions. | Per-org PATs, per-org GitHub App private keys |
Personal credential category
Use env-name personal for any credential tied to a single human identity — Entra UPNs, personal GitHub login passwords, MFA recovery codes.
Rules for personal secrets:
- Never use
platformas the env-name for a human-tied credential.platformis reserved for service and automation accounts. - Descriptor must be
<platform>-<account-identifier>:platformis the service the account belongs to:entra,github,gitlab,ado,microsoft-account, orazure-portal.account-identifieris the full login, kebab-cased, with@and.replaced by-. Never an abbreviation.kris-hybridsolutions-cloudis correct;kjt,kris, orhcsare not.
- Username and password must always be stored as a pair. Names differ only in the final
<field>segment. lifecycle=temporarytag is required. Default expiry: 30 days.- Long-term recommendation: migrate all
personalsecrets to a dedicatedkv-hcs-personal-01vault so personal logins are not co-mingled with automation credentials.
Examples:
| Secret name | Field | What it is |
|---|---|---|
hcs-personal-entra-kris-hybridsolutions-cloud-username | username | Entra UPN: kris@hybridsolutions.cloud |
hcs-personal-entra-kris-hybridsolutions-cloud-password | password | Password for kris@hybridsolutions.cloud |
hcs-personal-entra-kristopher-turner-azurelocal-cloud-username | username | Entra UPN: kristopher.turner@azurelocal.cloud |
hcs-personal-entra-kristopher-turner-azurelocal-cloud-password | password | Password for kristopher.turner@azurelocal.cloud |
hcs-personal-github-kristopherjturner-username | username | GitHub login: kristopherjturner |
hcs-personal-github-kristopherjturner-password | password | GitHub account password for kristopherjturner |
Descriptor construction — Azure Local accounts
Azure Local secrets must include the cluster name in the descriptor so secrets from different clusters in the same environment never collide.
azl-<cluster-name>-<account-type>-<instance>| Segment | Rules |
|---|---|
azl | Always first for Azure Local workload secrets |
cluster-name | The actual name of the cluster — short, matches what is registered in Azure Arc |
account-type | local-admin, deployment, lcm, domain-admin |
instance | Zero-padded number for account types that have multiple instances (lcm-01, lcm-02). Omit for single-instance account types. |
Examples:
tp-tplabs-azl-hci01-lcm-01-password— tplabs env, clusterhci01, LCM account 01, passwordtp-tplabs-azl-hci01-lcm-02-password— same cluster, LCM account 02tp-tplabs-azl-hci02-lcm-01-password— same env, different clusterhci02, LCM account 01tp-tplabs-azl-hci01-local-admin-password— clusterhci01, local admin passwordtp-tplabs-azl-hci01-deployment-password— clusterhci01, deployment account password
Vault assignment
Each organisation owns its own Key Vault. There is no single universal vault — kv-hcs-vault-01 is the hcs-scope default, used for the LLC, personal projects, blogs, and community work. It is not assumed for any other scope.
| Org | Vault |
|---|---|
hcs, ccb, ktj, azl and other personal orgs (hcs scope) | kv-hcs-vault-01 |
tp (TierPoint, tierpoint-prodtech scope) | TierPoint's KV in their Azure subscription |
| Other clients or employers | Their own dedicated KV |
The secret inventory in this repo is the authoritative registry for all secrets across all vaults. Every entry records which vault the secret lives in. The platform managed identity has read and write access to external vaults, so scripts and pipelines can resolve any registered secret regardless of which vault holds it.
Example secret names by account type
The examples below show real secret names as they would appear in the inventory. The naming pattern is the same regardless of which vault holds the secret — the inventory records the vault location for each entry.
Domain and Active Directory accounts
| Secret name | What it is |
|---|---|
tp-tplabs-domain-admin-username | Domain administrator account name for tplabs AD |
tp-tplabs-domain-admin-password | Domain administrator password for tplabs AD |
hcs-hcsprod-domain-admin-username | Domain administrator account name for HCS production AD |
hcs-hcsprod-domain-admin-password | Domain administrator password for HCS production AD |
Azure Local — node and deployment accounts
| Secret name | What it is |
|---|---|
tp-tplabs-azl-hci01-local-admin-username | Local admin account name on cluster hci01 nodes |
tp-tplabs-azl-hci01-local-admin-password | Local admin password on cluster hci01 nodes |
tp-tplabs-azl-hci01-deployment-username | Deployment account name for cluster hci01 |
tp-tplabs-azl-hci01-deployment-password | Deployment account password for cluster hci01 |
hcs-hcsprod-azl-hci01-local-admin-username | HCS production cluster hci01 local admin — username |
hcs-hcsprod-azl-hci01-local-admin-password | HCS production cluster hci01 local admin — password |
Azure Local — LCM accounts
Azure Local requires up to 15 LCM managed accounts per cluster. Each is numbered starting at 01.
| Secret name | What it is |
|---|---|
tp-tplabs-azl-hci01-lcm-01-username | Cluster hci01 LCM account 01 — username |
tp-tplabs-azl-hci01-lcm-01-password | Cluster hci01 LCM account 01 — password |
tp-tplabs-azl-hci01-lcm-02-username | Cluster hci01 LCM account 02 — username |
tp-tplabs-azl-hci01-lcm-02-password | Cluster hci01 LCM account 02 — password |
tp-tplabs-azl-hci02-lcm-01-username | Cluster hci02 LCM account 01 — username (different cluster, same env) |
tp-tplabs-azl-hci02-lcm-01-password | Cluster hci02 LCM account 01 — password |
| (continue through lcm-15 per cluster as needed) | |
hcs-hcsprod-azl-hci01-lcm-01-username | HCS production cluster hci01 LCM account 01 — username |
hcs-hcsprod-azl-hci01-lcm-01-password | HCS production cluster hci01 LCM account 01 — password |
Service principals
The purpose comes first in the descriptor — you should know what it does before you know it is an SPN.
| Secret name | What it is |
|---|---|
tp-tplabs-arc-registration-spn-client-id | SPN for Arc node registration in tplabs — client ID |
tp-tplabs-arc-registration-spn-secret | SPN for Arc node registration in tplabs — client secret |
hcs-hcsprod-arc-registration-spn-client-id | SPN for Arc node registration in HCS production — client ID |
hcs-hcsprod-arc-registration-spn-secret | SPN for Arc node registration in HCS production — client secret |
hcs-platform-ado-pipelines-spn-client-id | SPN used by ADO pipelines for Azure operations — client ID |
hcs-platform-ado-pipelines-spn-secret | SPN used by ADO pipelines for Azure operations — client secret |
hcs-platform-swa-deploy-spn-client-id | SPN for Static Web Apps deployment — client ID |
hcs-platform-swa-deploy-spn-secret | SPN for Static Web Apps deployment — client secret |
Managed identities
Managed identities authenticate via Azure AD and do not have secrets. Store the client ID in KV so scripts and pipelines can resolve it without hardcoding.
| Secret name | What it is |
|---|---|
hcs-platform-platform-automation-mi-client-id | Client ID of the platform automation managed identity |
tp-tplabs-arc-management-mi-client-id | Client ID of the Arc management managed identity in tplabs |
hcs-hcsprod-backup-mi-client-id | Client ID of the backup managed identity in HCS production |
API keys and PATs
| Secret name | What it is |
|---|---|
hcs-platform-github-org-pat | GitHub PAT with access to all HCS GitHub organisations |
hcs-platform-ado-platform-pat | ADO PAT for the Platform Engineering org |
hcs-platform-anthropic-api-key | Anthropic API key for Claude Code and SDK usage |
hcs-platform-cloudflare-api-token | Cloudflare API token scoped to hybridsolutions.cloud DNS |
tp-platform-github-org-pat | GitHub PAT scoped to TierPoint GitHub orgs |
tp-platform-ado-platform-pat | ADO PAT for TierPoint ADO org |
Private keys and certificates
Private keys stored as KV Secrets (PEM or base64 encoded):
| Secret name | What it is |
|---|---|
hcs-platform-wildcard-hybridsolutions-cloud-private-key | Private key for *.hybridsolutions.cloud wildcard cert |
hcs-platform-platform-site-private-key | Private key for platform.hybridsolutions.cloud cert |
tp-tplabs-ado-agent-mtls-private-key | Private key for mTLS cert used by ADO self-hosted agents |
Certificates managed natively in KV (preferred — KV handles renewal):
| Certificate name | What it is |
|---|---|
hcs-platform-wildcard-hybridsolutions-cloud-cert | Wildcard TLS certificate for *.hybridsolutions.cloud |
hcs-platform-platform-site-cert | TLS certificate for platform.hybridsolutions.cloud |
tp-tplabs-ado-agent-mtls-cert | mTLS certificate for tplabs ADO self-hosted agents |
Cryptographic keys (KV Key type)
Used for encryption operations only — not for storing credential values.
| Key name | Use |
|---|---|
hcs-hcsprod-disk-encryption-key | Customer-managed key for managed disk encryption |
hcs-hcsprod-backup-encryption-key | Encryption key for Azure Backup |
hcs-hcsprod-storage-cmk | Customer-managed key for storage account encryption |
tp-tplabs-disk-encryption-key | Customer-managed key for tplabs managed disk encryption |
Key type: RSA 4096 or EC P-256. RSA 2048 minimum. Never RSA 1024.
Required attributes
Expiry date
Every secret must be created with an expiry date. Key Vault will alert before expiry.
$expiry = (Get-Date).AddDays(90).ToString("yyyy-MM-ddTHH:mm:ssZ")
az keyvault secret set `
--vault-name kv-hcs-vault-01 `
--name hcs-platform-github-org-pat `
--value $secretValue `
--expires $expiryDefault expiry windows:
| Type | Expiry |
|---|---|
| PATs and API tokens with broad access | 90 days |
| API keys for SaaS services | 180 days |
| Service principal secrets | 180 days |
| Infrastructure account passwords | 365 days or on change |
| Certificates | 365 days — renew before expiry |
Tags
Every object must carry these five tags:
| Tag | Description | Example values |
|---|---|---|
owner | Email of the person responsible | kris@hybridsolutions.cloud |
project | Project or workload this belongs to | platform-engineering, tplabs-azurelocal |
rotation-days | Rotation frequency in days | 30, 90, 180, 365 |
managed-by | How it is created and updated | manual, ado-pipeline, script |
lifecycle | Lifetime intent of this secret | permanent, temporary |
Any secret in env-name personal must have lifecycle=temporary. Any secret that is a permanent service credential must have lifecycle=permanent. Omitting lifecycle is prohibited.
az keyvault secret set `
--vault-name kv-hcs-vault-01 `
--name hcs-platform-github-org-pat `
--value $secretValue `
--expires $expiry `
--tags owner=kris@hybridsolutions.cloud project=platform-engineering rotation-days=90 managed-by=manual lifecycle=permanentLifecycle
Adding a new secret
- Choose the naming pattern and confirm the name is unique and descriptive
- Create the secret in the correct vault with expiry and all four required tags
- Register it in secret-inventory.md — the entry must include the vault name
- If it feeds a pipeline, register it in the ADO variable group
- If it is needed in local sessions, add it to
scripts/Load-HCSEnvironment.ps1 - Commit:
docs(identity): register secret <secret-name>
Rotating a secret
- Generate the new value in the target system
- Update KV:
az keyvault secret set --vault-name <vault> --name <name>with the new value and updated expiry - Update the
Last rotateddate in secret-inventory.md - Verify all consumers still function
- Commit the inventory update
Retiring a secret
- Confirm no consumers are using the secret
- Delete from KV:
az keyvault secret delete --vault-name <vault> --name <name> - Move the inventory row to the "Expired or revoked" table with deletion date and reason
- Remove from
Load-HCSEnvironment.ps1if present - Commit:
docs(identity): retire secret <secret-name>
Secret inventory requirements
The secret inventory at secret-inventory.md is the authoritative registry for all secrets across all vaults. Every entry must include:
| Column | Rule |
|---|---|
| Secret name | Full name as it exists in KV |
| Vault | The Key Vault resource name where the secret lives (e.g. kv-hcs-vault-01, kv-tp-tplabs-01) |
| Purpose | What the secret is for |
| Consumers | Every script, pipeline, or service that reads this secret — if empty, candidate for deletion |
| Rotation schedule | How often it must be rotated |
| Last rotated | Date last rotated — must be updated on every rotation |
An unregistered secret found in any vault during an audit must be registered immediately or deleted.
Access control
Per-vault access
- Access is controlled by Azure RBAC — not vault access policies
- Minimum role to read:
Key Vault Secrets User - Minimum role to create or update:
Key Vault Secrets Officer - No SPN or pipeline should have
Key Vault Administrator
Cross-vault access — platform managed identity
The platform managed identity is the mechanism by which HCS tooling and pipelines access secrets in vaults it does not own (TierPoint, customer vaults, etc.).
- The platform MI must be granted
Key Vault Secrets Useron any external vault it needs to read - If platform tooling needs to write secrets to an external vault, it must be granted
Key Vault Secrets Officeron that vault - The MI's access grants must be documented in managed-identities.md — each vault it has access to, the role, and the reason
- No pipeline or script should be granted direct access to an external vault using a SPN secret — use the platform MI
keyvault:// URI across vaults
Config YAML references secrets using the keyvault:// URI scheme. The vault name in the URI must exactly match the vault where the secret lives:
# HCS vault secret
local_admin_password: keyvault://kv-hcs-vault-01/hcs-hcsprod-azl-hci01-local-admin-password
# TierPoint vault secret (resolved via platform MI access)
local_admin_password: keyvault://kv-tp-tplabs-01/tp-tplabs-azl-hci01-local-admin-passwordThe Resolve-KeyVaultRef function in platform scripts must support specifying the vault by name from the URI — it cannot assume all secrets live in kv-hcs-vault-01.
Prohibited practices
- Names without an org prefix:
github-pat,domain-admin-password - Generic names without a descriptor:
password,secret,token,api-key - Including region in the name:
tp-eus-tplabs-domain-admin-password - Using generic env type instead of env name:
tp-poc-domain-admin-password - Naming SPNs with the type first:
spn-arc-registration— purpose always comes first - camelCase or PascalCase:
tpLabsDomainAdminPassword - Creating secrets without an expiry date
- Creating secrets without the five required tags (including
lifecycle) - Committing secret values to any file in any repo
- Using an env-name not present in the env-name registry
- Using
platformas the env-name for any human-tied credential - Abbreviating a human account identifier —
kjt,azl,hcsare never valid identifiers for a person - Storing a password without a paired username secret
- Using a
fieldvalue not in the approved field values table
Worked example: renaming personal credentials
The three secrets below were created under old ad-hoc naming. They have been renamed to comply with this standard. The table records the before/after for auditability; old names are listed as "Expired or revoked" in the secret inventory.
| Old name | New username secret | New password secret |
|---|---|---|
hcs-platform-github-kjt-password | hcs-personal-github-kristopherjturner-username | hcs-personal-github-kristopherjturner-password |
hcs-platform-entra-hcs-password | hcs-personal-entra-kris-hybridsolutions-cloud-username | hcs-personal-entra-kris-hybridsolutions-cloud-password |
hcs-platform-entra-azl-password | hcs-personal-entra-kristopher-turner-azurelocal-cloud-username | hcs-personal-entra-kristopher-turner-azurelocal-cloud-password |
Why the old names failed:
kjt,hcs, andazlare abbreviations — unrecognisable to anyone who did not create them.platformis the wrong env-name for a human login —platformis reserved for automation credentials.- No username counterpart was created alongside any of the passwords.