Summary
On the azure stack, two config keys that differ only in case — e.g. RECALL_API_KEY vs Recall_Api_Key — are stored as the same Azure Key Vault secret and silently overwrite
each other. defang config set reports success for both, but only the last write survives. Its recorded key becomes the surviving casing, so ListConfig no longer returns the
original name and deploy fails with a misleading error:
Error: missing configs ["RECALL_API_KEY"] (https://s.defang.io/config)
This is azure-only. AWS (SSM Parameter Store) and GCP (Secret Manager) store config names case-sensitively, so the same two names coexist correctly there.
Root cause
Azure Key Vault secret names are case-insensitive. Defang preserves case when encoding a config key into a secret name, and never checks for a collision before writing — so two
differently-cased keys collapse onto one secret on Azure's side.
- Encoding preserves case —
ToSecretName only maps _→-; it does not change case. (The case-folding is done by Key Vault, not Defang.)
src/pkg/clouds/azure/keyvault/keyvault.go:48
func ToSecretName(key string) string {
return strings.ReplaceAll(key, "_", "-")
}
RECALL_API_KEY → RECALL-API-KEY and Recall_Api_Key → Recall-Api-Key are distinct strings, but Key Vault treats them as the same secret.
- Write does no collision check — last write wins. SetSecret overwrites the existing secret and its defang-config tag.
src/pkg/clouds/azure/keyvault/keyvault.go:311 (PutSecret), src/pkg/cli/client/byoc/azure/byoc.go:730-742 (PutConfig)
- ListConfig returns the surviving tag's casing (Recall_Api_Key, not RECALL_API_KEY).
src/pkg/cli/client/byoc/azure/byoc.go:685-709
- Validation matches case-sensitively, so the original compose reference is reported missing.
src/pkg/cli/compose/validation.go:473 (slices.Contains), also src/pkg/cli/configResolution.go:48
Reproduction
On the azure stack, with a compose file referencing ${RECALL_API_KEY}:
defang config set RECALL_API_KEY # value A
defang config set Recall_Api_Key # value B (different case) — silently overwrites A
defang config # lists only ONE entry
defang compose up # Error: missing configs ["RECALL_API_KEY"]
Expected behavior
Since Key Vault cannot represent both names, defang config set should fail loudly on the collision (e.g. config "Recall_Api_Key" conflicts with existing "RECALL_API_KEY") instead of
silently overwriting and failing later at deploy.
Summary
On the azure stack, two config keys that differ only in case — e.g.
RECALL_API_KEYvsRecall_Api_Key— are stored as the same Azure Key Vault secret and silently overwriteeach other.
defang config setreports success for both, but only the last write survives. Its recorded key becomes the surviving casing, soListConfigno longer returns theoriginal name and deploy fails with a misleading error:
Error: missing configs ["RECALL_API_KEY"] (https://s.defang.io/config)This is azure-only. AWS (SSM Parameter Store) and GCP (Secret Manager) store config names case-sensitively, so the same two names coexist correctly there.
Root cause
Azure Key Vault secret names are case-insensitive. Defang preserves case when encoding a config key into a secret name, and never checks for a collision before writing — so two
differently-cased keys collapse onto one secret on Azure's side.
ToSecretNameonly maps_→-; it does not change case. (The case-folding is done by Key Vault, not Defang.)src/pkg/clouds/azure/keyvault/keyvault.go:48src/pkg/clouds/azure/keyvault/keyvault.go:311 (PutSecret), src/pkg/cli/client/byoc/azure/byoc.go:730-742 (PutConfig)
src/pkg/cli/client/byoc/azure/byoc.go:685-709
src/pkg/cli/compose/validation.go:473 (slices.Contains), also src/pkg/cli/configResolution.go:48
Reproduction
On the azure stack, with a compose file referencing ${RECALL_API_KEY}:
Expected behavior
Since Key Vault cannot represent both names, defang config set should fail loudly on the collision (e.g. config "Recall_Api_Key" conflicts with existing "RECALL_API_KEY") instead of
silently overwriting and failing later at deploy.