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:
@@ -4762,6 +4762,13 @@
|
|||||||
"vault.config_error_title": "Vault Not Configured",
|
"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_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.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.type_file": "File",
|
||||||
"vault.compare": "Compare",
|
"vault.compare": "Compare",
|
||||||
"vault.compare_version": "Compare this version",
|
"vault.compare_version": "Compare this version",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package vault
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
unit "code.gitcaddy.com/server/v3/models/unit"
|
unit "code.gitcaddy.com/server/v3/models/unit"
|
||||||
"code.gitcaddy.com/server/v3/modules/web"
|
"code.gitcaddy.com/server/v3/modules/web"
|
||||||
@@ -73,6 +74,8 @@ func setVaultLicenseData(ctx *context.Context) {
|
|||||||
// Configuration status (for admin warnings)
|
// Configuration status (for admin warnings)
|
||||||
ctx.Data["VaultConfigured"] = vault_service.IsConfigured()
|
ctx.Data["VaultConfigured"] = vault_service.IsConfigured()
|
||||||
ctx.Data["VaultConfigError"] = vault_service.GetConfigurationError()
|
ctx.Data["VaultConfigError"] = vault_service.GetConfigurationError()
|
||||||
|
ctx.Data["VaultUsingFallbackKey"] = vault_service.IsUsingFallbackKey()
|
||||||
|
ctx.Data["VaultKeySource"] = vault_service.KeySource()
|
||||||
}
|
}
|
||||||
|
|
||||||
// List displays the vault secrets list
|
// List displays the vault secrets list
|
||||||
@@ -82,8 +85,18 @@ func List(ctx *context.Context) {
|
|||||||
return
|
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)
|
secrets, err := vault_service.ListSecrets(ctx, ctx.Repo.Repository.ID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if isCryptoError(err) {
|
||||||
|
showConfigError(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.ServerError("ListSecrets", err)
|
ctx.ServerError("ListSecrets", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -144,6 +157,10 @@ func View(ctx *context.Context) {
|
|||||||
// Get decrypted value for display
|
// Get decrypted value for display
|
||||||
value, err := vault_service.GetSecretValue(ctx, ctx.Repo.Repository.ID, name, 0) // 0 = current version
|
value, err := vault_service.GetSecretValue(ctx, ctx.Repo.Repository.ID, name, 0) // 0 = current version
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if isCryptoError(err) {
|
||||||
|
showConfigError(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.ServerError("GetSecretValue", err)
|
ctx.ServerError("GetSecretValue", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -207,6 +224,10 @@ func CreateSecret(ctx *context.Context) {
|
|||||||
CreatorID: ctx.Doer.ID,
|
CreatorID: ctx.Doer.ID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if isCryptoError(err) {
|
||||||
|
showConfigError(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.ServerError("CreateSecret", err)
|
ctx.ServerError("CreateSecret", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -236,6 +257,10 @@ func UpdateSecret(ctx *context.Context) {
|
|||||||
ctx.NotFound(nil)
|
ctx.NotFound(nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if isCryptoError(err) {
|
||||||
|
showConfigError(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.ServerError("UpdateSecret", err)
|
ctx.ServerError("UpdateSecret", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -485,3 +510,17 @@ func showPluginNotAvailable(ctx *context.Context) {
|
|||||||
ctx.Data["VaultPluginNotInstalled"] = true
|
ctx.Data["VaultPluginNotInstalled"] = true
|
||||||
ctx.HTML(http.StatusOK, "repo/vault/not_installed")
|
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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ type ConfigurablePlugin interface {
|
|||||||
IsConfigured() bool
|
IsConfigured() bool
|
||||||
// ConfigurationError returns the configuration error message, if any
|
// ConfigurationError returns the configuration error message, if any
|
||||||
ConfigurationError() string
|
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
|
// Secret represents a vault secret
|
||||||
@@ -199,6 +204,33 @@ func GetConfigurationError() string {
|
|||||||
return ""
|
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
|
// GetLicenseInfo returns the license info for the vault plugin
|
||||||
// Returns default Solo license if no license file is present
|
// Returns default Solo license if no license file is present
|
||||||
func GetLicenseInfo() *plugins.LicenseInfo {
|
func GetLicenseInfo() *plugins.LicenseInfo {
|
||||||
|
|||||||
42
templates/repo/vault/config_error.tmpl
Normal file
42
templates/repo/vault/config_error.tmpl
Normal 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" .}}
|
||||||
@@ -12,6 +12,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{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="ui segment">
|
||||||
<div class="tw-flex tw-items-center tw-justify-between tw-w-full">
|
<div class="tw-flex tw-items-center tw-justify-between tw-w-full">
|
||||||
<h4 class="ui header tw-mb-0">
|
<h4 class="ui header tw-mb-0">
|
||||||
|
|||||||
Reference in New Issue
Block a user