Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f00027eb7c | |||
| e88d9f2e82 | |||
| 2aaf7223f1 | |||
| d9c35526bc |
@@ -12,7 +12,9 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GOPROXY: https://proxy.golang.org,direct
|
||||
GOPROXY: direct
|
||||
GOPRIVATE: git.marketally.com,code.gitcaddy.com
|
||||
GONOSUMDB: git.marketally.com,code.gitcaddy.com
|
||||
GO_VERSION: "1.25"
|
||||
|
||||
jobs:
|
||||
@@ -29,6 +31,10 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure git for private modules
|
||||
run: |
|
||||
git config --global url."https://token:${{ secrets.RELEASE_TOKEN }}@git.marketally.com/".insteadOf "https://git.marketally.com/"
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
@@ -58,6 +64,10 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure git for private modules
|
||||
run: |
|
||||
git config --global url."https://token:${{ secrets.RELEASE_TOKEN }}@git.marketally.com/".insteadOf "https://git.marketally.com/"
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
@@ -27,7 +28,9 @@ var (
|
||||
|
||||
// Manager handles encryption operations using the master KEK
|
||||
type Manager struct {
|
||||
masterKey []byte
|
||||
masterKey []byte
|
||||
usingFallback bool // true when using Gitea SECRET_KEY as fallback
|
||||
keySource string
|
||||
}
|
||||
|
||||
// NewManager creates a new crypto manager
|
||||
@@ -39,14 +42,27 @@ func NewManager() *Manager {
|
||||
func (m *Manager) LoadMasterKey() error {
|
||||
// Priority: app.ini [vault] > env var > file > gitea secret key
|
||||
key := m.loadFromSettings()
|
||||
if key != nil {
|
||||
m.keySource = "app.ini [vault] MASTER_KEY"
|
||||
}
|
||||
if key == nil {
|
||||
key = m.loadFromEnv()
|
||||
if key != nil {
|
||||
m.keySource = "GITCADDY_VAULT_KEY environment variable"
|
||||
}
|
||||
}
|
||||
if key == nil {
|
||||
key = m.loadFromFile()
|
||||
if key != nil {
|
||||
m.keySource = "key file"
|
||||
}
|
||||
}
|
||||
if key == nil {
|
||||
key = m.loadFromGiteaSecret()
|
||||
if key != nil {
|
||||
m.keySource = "Gitea SECRET_KEY (fallback)"
|
||||
m.usingFallback = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(key) == 0 {
|
||||
@@ -64,7 +80,7 @@ func (m *Manager) LoadMasterKey() error {
|
||||
}
|
||||
|
||||
m.masterKey = key
|
||||
log.Info("Vault master key loaded successfully")
|
||||
log.Info("Vault master key loaded successfully (source: %s)", m.keySource)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -78,7 +94,17 @@ func (m *Manager) loadFromSettings() []byte {
|
||||
if keyStr == "" {
|
||||
return nil
|
||||
}
|
||||
log.Info("Vault master key loaded from app.ini [vault] section")
|
||||
|
||||
// Try to hex-decode the key (expected format: 64 hex chars = 32 bytes)
|
||||
if len(keyStr) == 64 {
|
||||
if decoded, err := hex.DecodeString(keyStr); err == nil {
|
||||
log.Info("Vault master key loaded from app.ini [vault] section (hex-decoded)")
|
||||
return decoded
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to raw bytes if not valid hex
|
||||
log.Info("Vault master key loaded from app.ini [vault] section (raw)")
|
||||
return []byte(keyStr)
|
||||
}
|
||||
|
||||
@@ -87,6 +113,14 @@ func (m *Manager) loadFromEnv() []byte {
|
||||
if keyStr == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to hex-decode the key (expected format: 64 hex chars = 32 bytes)
|
||||
if len(keyStr) == 64 {
|
||||
if decoded, err := hex.DecodeString(keyStr); err == nil {
|
||||
return decoded
|
||||
}
|
||||
}
|
||||
|
||||
return []byte(keyStr)
|
||||
}
|
||||
|
||||
@@ -116,7 +150,16 @@ func (m *Manager) loadFromFile() []byte {
|
||||
}
|
||||
|
||||
// Trim whitespace
|
||||
return []byte(strings.TrimSpace(string(key)))
|
||||
keyStr := strings.TrimSpace(string(key))
|
||||
|
||||
// Try to hex-decode the key (expected format: 64 hex chars = 32 bytes)
|
||||
if len(keyStr) == 64 {
|
||||
if decoded, err := hex.DecodeString(keyStr); err == nil {
|
||||
return decoded
|
||||
}
|
||||
}
|
||||
|
||||
return []byte(keyStr)
|
||||
}
|
||||
|
||||
func (m *Manager) loadFromGiteaSecret() []byte {
|
||||
@@ -133,6 +176,17 @@ func (m *Manager) HasMasterKey() bool {
|
||||
return len(m.masterKey) > 0
|
||||
}
|
||||
|
||||
// IsUsingFallbackKey returns true if the master key was loaded from Gitea's SECRET_KEY
|
||||
// rather than an explicit vault-specific key configuration.
|
||||
func (m *Manager) IsUsingFallbackKey() bool {
|
||||
return m.usingFallback
|
||||
}
|
||||
|
||||
// KeySource returns a human-readable description of where the master key was loaded from.
|
||||
func (m *Manager) KeySource() string {
|
||||
return m.keySource
|
||||
}
|
||||
|
||||
// Encrypt encrypts plaintext using AES-256-GCM
|
||||
// Returns: nonce || ciphertext || tag
|
||||
func (m *Manager) Encrypt(plaintext []byte, key []byte) ([]byte, error) {
|
||||
@@ -239,6 +293,16 @@ func HasMasterKey() bool {
|
||||
return defaultManager.HasMasterKey()
|
||||
}
|
||||
|
||||
// IsUsingFallbackKey checks if the default manager is using Gitea's SECRET_KEY as fallback
|
||||
func IsUsingFallbackKey() bool {
|
||||
return defaultManager.IsUsingFallbackKey()
|
||||
}
|
||||
|
||||
// KeySource returns the key source of the default manager
|
||||
func KeySource() string {
|
||||
return defaultManager.KeySource()
|
||||
}
|
||||
|
||||
// EncryptWithMasterKey encrypts using the default manager
|
||||
func EncryptWithMasterKey(plaintext []byte) ([]byte, error) {
|
||||
return defaultManager.EncryptWithMasterKey(plaintext)
|
||||
|
||||
@@ -11,6 +11,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.secret_name": "Name",
|
||||
"vault.secret_type": "Type",
|
||||
"vault.secret_value": "Secret Value",
|
||||
@@ -150,5 +157,13 @@
|
||||
"vault.run_compare": "Compare",
|
||||
"vault.unified_diff": "Unified Diff",
|
||||
"vault.back_to_versions": "Back to Versions",
|
||||
"vault.compare_same_version": "Cannot compare a version with itself"
|
||||
"vault.compare_same_version": "Cannot compare a version with itself",
|
||||
"vault.key_mismatch_title": "Encryption Key Mismatch",
|
||||
"vault.key_mismatch_message": "The vault encryption key has changed since these secrets were created.",
|
||||
"vault.key_mismatch_explanation": "Secrets in this vault were encrypted with a different master key than what is currently configured. This can happen if the MASTER_KEY in app.ini was changed, or if secrets were created before the key was set (using the fallback key).",
|
||||
"vault.key_mismatch_solutions_title": "Possible Solutions",
|
||||
"vault.key_mismatch_solution_1": "Restore the original MASTER_KEY that was used when secrets were created",
|
||||
"vault.key_mismatch_solution_2": "If using the fallback key previously, temporarily remove MASTER_KEY from [vault] section to use the original key",
|
||||
"vault.key_mismatch_solution_3": "Contact your administrator to migrate secrets to the new key",
|
||||
"vault.key_mismatch_admin_note": "Admin: Check app.ini [vault] MASTER_KEY setting and compare with the key used when secrets were originally created."
|
||||
}
|
||||
11
plugin.go
11
plugin.go
@@ -144,6 +144,17 @@ func (p *VaultPlugin) ConfigurationError() 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.
|
||||
func (p *VaultPlugin) IsUsingFallbackKey() bool {
|
||||
return crypto.IsUsingFallbackKey()
|
||||
}
|
||||
|
||||
// KeySource returns a human-readable description of where the master key was loaded from.
|
||||
func (p *VaultPlugin) KeySource() string {
|
||||
return crypto.KeySource()
|
||||
}
|
||||
|
||||
// Ensure VaultPlugin implements all required interfaces
|
||||
var (
|
||||
_ plugins.Plugin = (*VaultPlugin)(nil)
|
||||
|
||||
@@ -38,6 +38,7 @@ const (
|
||||
tplVaultCompare templates.TplName = "repo/vault/compare"
|
||||
tplVaultAudit templates.TplName = "repo/vault/audit"
|
||||
tplVaultTokens templates.TplName = "repo/vault/tokens"
|
||||
tplVaultKeyError templates.TplName = "repo/vault/key_error"
|
||||
)
|
||||
|
||||
// API Response types
|
||||
@@ -225,6 +226,19 @@ func getWebContext(r *http.Request) *context.Context {
|
||||
return context.GetWebContext(r.Context())
|
||||
}
|
||||
|
||||
// isKeyMismatchError returns true if the error indicates an encryption key mismatch
|
||||
func isKeyMismatchError(err error) bool {
|
||||
return err == services.ErrEncryptionFailed || err == services.ErrDecryptionFailed
|
||||
}
|
||||
|
||||
// showKeyMismatchError renders the key error template for encryption/decryption failures
|
||||
func showKeyMismatchError(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("vault.key_mismatch_title")
|
||||
ctx.Data["PageIsVault"] = true
|
||||
ctx.Data["IsRepoAdmin"] = ctx.Repo.IsAdmin()
|
||||
ctx.HTML(http.StatusConflict, tplVaultKeyError)
|
||||
}
|
||||
|
||||
func requireRepoAdmin(ctx *context.APIContext) bool {
|
||||
if !ctx.Repo.IsAdmin() {
|
||||
ctx.JSON(http.StatusForbidden, map[string]any{
|
||||
@@ -458,6 +472,13 @@ func apiGetSecret(lic *license.Manager) http.HandlerFunc {
|
||||
value, err := services.GetSecretValue(ctx, ctx.Repo.Repository.ID, name, version)
|
||||
if err != nil {
|
||||
log.Error("Failed to decrypt secret %s: %v", name, err)
|
||||
if isKeyMismatchError(err) {
|
||||
ctx.JSON(http.StatusConflict, map[string]any{
|
||||
"error": "key_mismatch",
|
||||
"message": "Encryption key mismatch. The vault master key may have changed since secrets were created.",
|
||||
})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]any{
|
||||
"error": "decryption_failed",
|
||||
"message": "Failed to decrypt secret",
|
||||
@@ -563,6 +584,11 @@ func apiPutSecret(lic *license.Manager) http.HandlerFunc {
|
||||
"error": "limit_reached",
|
||||
"message": "Secret limit reached for this tier. Upgrade your license for more secrets.",
|
||||
})
|
||||
case services.ErrEncryptionFailed, services.ErrDecryptionFailed:
|
||||
ctx.JSON(http.StatusConflict, map[string]any{
|
||||
"error": "key_mismatch",
|
||||
"message": "Encryption key mismatch. The vault master key may not be configured correctly.",
|
||||
})
|
||||
default:
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]any{
|
||||
"error": "internal_error",
|
||||
@@ -595,6 +621,13 @@ func apiPutSecret(lic *license.Manager) http.HandlerFunc {
|
||||
UpdaterID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
if isKeyMismatchError(err) {
|
||||
ctx.JSON(http.StatusConflict, map[string]any{
|
||||
"error": "key_mismatch",
|
||||
"message": "Encryption key mismatch. The vault master key may have changed since secrets were created.",
|
||||
})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]any{
|
||||
"error": "internal_error",
|
||||
"message": err.Error(),
|
||||
@@ -838,6 +871,11 @@ func apiRollbackSecret(lic *license.Manager) http.HandlerFunc {
|
||||
"error": "version_not_found",
|
||||
"message": "Version not found",
|
||||
})
|
||||
case services.ErrEncryptionFailed, services.ErrDecryptionFailed:
|
||||
ctx.JSON(http.StatusConflict, map[string]any{
|
||||
"error": "key_mismatch",
|
||||
"message": "Encryption key mismatch. The vault master key may have changed since secrets were created.",
|
||||
})
|
||||
default:
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]any{
|
||||
"error": "internal_error",
|
||||
@@ -1292,6 +1330,10 @@ func webViewSecret(lic *license.Manager) http.HandlerFunc {
|
||||
value, err := services.GetSecretValue(ctx, ctx.Repo.Repository.ID, name, 0)
|
||||
if err != nil {
|
||||
log.Error("Failed to decrypt secret %s: %v", name, err)
|
||||
if isKeyMismatchError(err) {
|
||||
showKeyMismatchError(ctx)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetSecretValue", err)
|
||||
return
|
||||
}
|
||||
@@ -1380,6 +1422,10 @@ func webUpdateSecret(lic *license.Manager) http.HandlerFunc {
|
||||
UpdaterID: ctx.Doer.ID,
|
||||
})
|
||||
if err != nil {
|
||||
if isKeyMismatchError(err) {
|
||||
showKeyMismatchError(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Error(ctx.Tr("vault.error_update_failed"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/vault/secrets/" + name)
|
||||
return
|
||||
@@ -1483,6 +1529,8 @@ func webCreateSecret(lic *license.Manager) http.HandlerFunc {
|
||||
ctx.Data["description"] = description
|
||||
ctx.Flash.Error(ctx.Tr("vault.error_secret_limit"))
|
||||
ctx.HTML(http.StatusOK, tplVaultNew)
|
||||
case services.ErrEncryptionFailed, services.ErrDecryptionFailed:
|
||||
showKeyMismatchError(ctx)
|
||||
default:
|
||||
ctx.ServerError("CreateSecret", err)
|
||||
}
|
||||
@@ -1791,6 +1839,8 @@ func webRollbackSecret(lic *license.Manager) http.HandlerFunc {
|
||||
case services.ErrVersionNotFound:
|
||||
ctx.Flash.Error(ctx.Tr("vault.error_version_not_found"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/vault/secrets/" + name)
|
||||
case services.ErrEncryptionFailed, services.ErrDecryptionFailed:
|
||||
showKeyMismatchError(ctx)
|
||||
default:
|
||||
ctx.ServerError("RollbackSecret", err)
|
||||
}
|
||||
|
||||
27
templates/repo/vault/key_error.tmpl
Normal file
27
templates/repo/vault/key_error.tmpl
Normal file
@@ -0,0 +1,27 @@
|
||||
{{template "repo/vault/layout_head" (dict "ctxData" . "pageClass" "repository vault key-error")}}
|
||||
<div class="ui placeholder segment tw-text-center">
|
||||
<div class="ui icon header">
|
||||
{{svg "octicon-key" 48}}
|
||||
<h2>{{ctx.Locale.Tr "vault.key_mismatch_title"}}</h2>
|
||||
</div>
|
||||
|
||||
<div class="ui warning message" style="text-align: left; max-width: 600px; margin: 1em auto;">
|
||||
<div class="header">{{ctx.Locale.Tr "vault.key_mismatch_message"}}</div>
|
||||
<p style="margin-top: 0.5em;">{{ctx.Locale.Tr "vault.key_mismatch_explanation"}}</p>
|
||||
</div>
|
||||
|
||||
<div class="ui info message" style="text-align: left; max-width: 600px; margin: 1em auto;">
|
||||
<div class="header">{{ctx.Locale.Tr "vault.key_mismatch_solutions_title"}}</div>
|
||||
<ul class="ui list" style="margin-top: 0.5em;">
|
||||
<li>{{ctx.Locale.Tr "vault.key_mismatch_solution_1"}}</li>
|
||||
<li>{{ctx.Locale.Tr "vault.key_mismatch_solution_2"}}</li>
|
||||
<li>{{ctx.Locale.Tr "vault.key_mismatch_solution_3"}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{if .IsRepoAdmin}}
|
||||
<div class="ui divider"></div>
|
||||
<p class="ui small text grey">{{ctx.Locale.Tr "vault.key_mismatch_admin_note"}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "repo/vault/layout_footer" .}}
|
||||
Reference in New Issue
Block a user