2
0

feat(vault): add user-facing warnings for key configuration issues

Adds dedicated error page and warnings for vault encryption key problems including missing configuration, fallback key usage, and decryption failures. Displays context-specific messages to help users understand and fix key configuration issues. Includes detection of crypto errors in vault operations and graceful error handling throughout the UI.
This commit is contained in:
2026-02-04 13:54:54 -05:00
parent 53d4c529a7
commit 5c9385f4a2
5 changed files with 127 additions and 0 deletions

View File

@@ -4762,6 +4762,13 @@
"vault.config_error_title": "Vault Not Configured",
"vault.config_error_message": "The vault encryption key has not been configured. Secrets cannot be encrypted or decrypted.",
"vault.config_error_fix": "Add MASTER_KEY to the [vault] section in app.ini or set the GITCADDY_VAULT_KEY environment variable.",
"vault.fallback_key_warning_title": "Vault Using Fallback Encryption Key",
"vault.fallback_key_warning_message": "The vault is currently using Gitea's SECRET_KEY for encryption because no dedicated vault key has been configured. If the SECRET_KEY is ever changed or lost, all vault secrets will become permanently unreadable.",
"vault.fallback_key_warning_fix": "To fix this, copy the current SECRET_KEY value and set it as MASTER_KEY in the [vault] section of app.ini, or set the GITCADDY_VAULT_KEY environment variable. This ensures vault encryption remains stable even if the SECRET_KEY changes.",
"vault.decryption_error_title": "Vault Decryption Failed",
"vault.decryption_error_message": "Unable to decrypt vault secrets. The encryption key may have been changed or is incorrect.",
"vault.decryption_error_fix": "Verify that the MASTER_KEY in the [vault] section of app.ini (or the GITCADDY_VAULT_KEY environment variable) matches the key that was used when the secrets were originally created.",
"vault.encryption_error_message": "Unable to encrypt the secret value. The vault encryption key may not be configured correctly.",
"vault.type_file": "File",
"vault.compare": "Compare",
"vault.compare_version": "Compare this version",

View File

@@ -6,6 +6,7 @@ package vault
import (
"net/http"
"strconv"
"strings"
unit "code.gitcaddy.com/server/v3/models/unit"
"code.gitcaddy.com/server/v3/modules/web"
@@ -73,6 +74,8 @@ func setVaultLicenseData(ctx *context.Context) {
// Configuration status (for admin warnings)
ctx.Data["VaultConfigured"] = vault_service.IsConfigured()
ctx.Data["VaultConfigError"] = vault_service.GetConfigurationError()
ctx.Data["VaultUsingFallbackKey"] = vault_service.IsUsingFallbackKey()
ctx.Data["VaultKeySource"] = vault_service.KeySource()
}
// List displays the vault secrets list
@@ -82,8 +85,18 @@ func List(ctx *context.Context) {
return
}
// Check configuration status upfront so the list page can show a warning
if !vault_service.IsConfigured() {
showConfigError(ctx)
return
}
secrets, err := vault_service.ListSecrets(ctx, ctx.Repo.Repository.ID, false)
if err != nil {
if isCryptoError(err) {
showConfigError(ctx)
return
}
ctx.ServerError("ListSecrets", err)
return
}
@@ -144,6 +157,10 @@ func View(ctx *context.Context) {
// Get decrypted value for display
value, err := vault_service.GetSecretValue(ctx, ctx.Repo.Repository.ID, name, 0) // 0 = current version
if err != nil {
if isCryptoError(err) {
showConfigError(ctx)
return
}
ctx.ServerError("GetSecretValue", err)
return
}
@@ -207,6 +224,10 @@ func CreateSecret(ctx *context.Context) {
CreatorID: ctx.Doer.ID,
})
if err != nil {
if isCryptoError(err) {
showConfigError(ctx)
return
}
ctx.ServerError("CreateSecret", err)
return
}
@@ -236,6 +257,10 @@ func UpdateSecret(ctx *context.Context) {
ctx.NotFound(nil)
return
}
if isCryptoError(err) {
showConfigError(ctx)
return
}
ctx.ServerError("UpdateSecret", err)
return
}
@@ -485,3 +510,17 @@ func showPluginNotAvailable(ctx *context.Context) {
ctx.Data["VaultPluginNotInstalled"] = true
ctx.HTML(http.StatusOK, "repo/vault/not_installed")
}
// isCryptoError returns true if the error is a vault encryption or decryption failure
func isCryptoError(err error) bool {
msg := err.Error()
return strings.Contains(msg, "decryption failed") || strings.Contains(msg, "encryption failed")
}
// showConfigError shows a user-friendly error page for vault configuration/crypto issues
func showConfigError(ctx *context.Context) {
ctx.Data["PageIsVault"] = true
ctx.Data["VaultConfigured"] = vault_service.IsConfigured()
ctx.Data["IsRepoAdmin"] = ctx.Repo.IsAdmin()
ctx.HTML(http.StatusOK, "repo/vault/config_error")
}

View File

@@ -62,6 +62,11 @@ type ConfigurablePlugin interface {
IsConfigured() bool
// ConfigurationError returns the configuration error message, if any
ConfigurationError() string
// IsUsingFallbackKey returns true if the vault is using Gitea's SECRET_KEY as
// the encryption key instead of an explicit vault-specific key.
IsUsingFallbackKey() bool
// KeySource returns a human-readable description of where the master key was loaded from.
KeySource() string
}
// Secret represents a vault secret
@@ -199,6 +204,33 @@ func GetConfigurationError() string {
return ""
}
// IsUsingFallbackKey returns true if the vault is using Gitea's SECRET_KEY
// as the encryption key instead of an explicit vault-specific key.
// Returns false if the plugin doesn't implement the ConfigurablePlugin interface.
func IsUsingFallbackKey() bool {
vp := GetPlugin()
if vp == nil {
return false
}
if cp, ok := vp.(ConfigurablePlugin); ok {
return cp.IsUsingFallbackKey()
}
return false
}
// KeySource returns a human-readable description of where the master key was loaded from.
// Returns empty string if the plugin doesn't implement the ConfigurablePlugin interface.
func KeySource() string {
vp := GetPlugin()
if vp == nil {
return ""
}
if cp, ok := vp.(ConfigurablePlugin); ok {
return cp.KeySource()
}
return ""
}
// GetLicenseInfo returns the license info for the vault plugin
// Returns default Solo license if no license file is present
func GetLicenseInfo() *plugins.LicenseInfo {

View File

@@ -0,0 +1,42 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository vault">
{{template "repo/header" .}}
<div class="ui container">
<div class="ui center aligned segment">
<div class="ui icon header">
{{svg "octicon-alert" 64}}
<div class="content">
{{if .VaultConfigured}}
{{ctx.Locale.Tr "vault.decryption_error_title"}}
{{else}}
{{ctx.Locale.Tr "vault.config_error_title"}}
{{end}}
<div class="sub header">GitCaddy Vault</div>
</div>
</div>
<div class="ui warning message" style="text-align: left;">
<div class="header">
{{if .VaultConfigured}}
{{ctx.Locale.Tr "vault.decryption_error_message"}}
{{else}}
{{ctx.Locale.Tr "vault.config_error_message"}}
{{end}}
</div>
<p style="margin-top: 0.5em;">
{{if .VaultConfigured}}
{{ctx.Locale.Tr "vault.decryption_error_fix"}}
{{else}}
{{ctx.Locale.Tr "vault.config_error_fix"}}
{{end}}
</p>
</div>
{{if and .IsRepoAdmin (not .VaultConfigured)}}
<div class="ui divider"></div>
<p class="ui small text grey">{{ctx.Locale.Tr "vault.config_error_fix"}}</p>
{{end}}
</div>
</div>
</div>
{{template "base/footer" .}}

View File

@@ -12,6 +12,13 @@
</div>
</div>
{{else}}
{{if .VaultUsingFallbackKey}}
<div class="ui warning message">
<div class="header">{{svg "octicon-alert" 16}} {{ctx.Locale.Tr "vault.fallback_key_warning_title"}}</div>
<p>{{ctx.Locale.Tr "vault.fallback_key_warning_message"}}</p>
{{if .IsRepoAdmin}}<p class="tw-text-sm">{{ctx.Locale.Tr "vault.fallback_key_warning_fix"}}</p>{{end}}
</div>
{{end}}
<div class="ui segment">
<div class="tw-flex tw-items-center tw-justify-between tw-w-full">
<h4 class="ui header tw-mb-0">