feat(auth): support per-deploy login password key override
Deployment Config CI / Deployment Config (push) Successful in 27s
Backend CI / Backend (push) Successful in 15m4s

Multi-replica deployments need every Pod/container to share one RSA private key, otherwise public keys handed to the browser may not match the key that decrypts the resulting ciphertext. Introduce a `*.local.yaml` override layer that ops-config and tenant-config both load on top of the base config, ship example overrides plus compose/k3s wiring, and tolerate PEM bodies that arrive folded or escaped from Secrets.
This commit is contained in:
2026-05-14 11:54:40 +08:00
parent 2bc192b3c7
commit b939cfa2fa
16 changed files with 310 additions and 10 deletions
+46 -1
View File
@@ -96,7 +96,7 @@ func (c *PasswordCipher) Decrypt(ciphertextB64, keyID string) (string, error) {
}
func parseRSAPrivateKeyPEM(value string) (*rsa.PrivateKey, error) {
trimmed := strings.TrimSpace(value)
trimmed := normalizePrivateKeyPEM(value)
if trimmed == "" {
return nil, fmt.Errorf("password cipher private key is required")
}
@@ -126,3 +126,48 @@ func parseRSAPrivateKeyPEM(value string) (*rsa.PrivateKey, error) {
return nil, fmt.Errorf("unsupported private key PEM type %q", block.Type)
}
}
func normalizePrivateKeyPEM(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return ""
}
normalized := strings.ReplaceAll(trimmed, "\r\n", "\n")
normalized = strings.ReplaceAll(normalized, "\r", "\n")
normalized = strings.ReplaceAll(normalized, `\n`, "\n")
if block, _ := pem.Decode([]byte(normalized)); block != nil {
return normalized
}
for _, blockType := range []string{"RSA PRIVATE KEY", "PRIVATE KEY"} {
begin := "-----BEGIN " + blockType + "-----"
end := "-----END " + blockType + "-----"
start := strings.Index(normalized, begin)
stop := strings.Index(normalized, end)
if start < 0 || stop <= start {
continue
}
body := normalized[start+len(begin) : stop]
body = strings.Join(strings.Fields(body), "")
if body == "" {
return normalized
}
return begin + "\n" + wrapBase64Lines(body, 64) + "\n" + end + "\n"
}
return normalized
}
func wrapBase64Lines(value string, width int) string {
if width <= 0 || len(value) <= width {
return value
}
var b strings.Builder
b.Grow(len(value) + len(value)/width)
for len(value) > width {
b.WriteString(value[:width])
b.WriteByte('\n')
value = value[width:]
}
b.WriteString(value)
return b.String()
}
@@ -7,6 +7,7 @@ import (
"crypto/x509"
"encoding/base64"
"encoding/pem"
"strings"
"testing"
"github.com/stretchr/testify/require"
@@ -62,3 +63,53 @@ func TestNewEphemeralPasswordCipherExposesPublicKey(t *testing.T) {
require.Equal(t, "RSA-OAEP-256", publicKey.Algorithm)
require.Contains(t, publicKey.PublicKey, "BEGIN PUBLIC KEY")
}
func TestPasswordCipherAcceptsFoldedPrivateKeyPEM(t *testing.T) {
t.Parallel()
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
privatePEM := string(pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: mustMarshalPKCS8PrivateKey(t, key),
}))
foldedPEM := strings.Join(strings.Fields(privatePEM), " ")
_, err = NewPasswordCipher(foldedPEM, "folded")
require.NoError(t, err)
}
func TestPasswordCipherAcceptsEscapedNewlinePrivateKeyPEM(t *testing.T) {
t.Parallel()
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
privatePEM := string(pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: mustMarshalPKCS8PrivateKey(t, key),
}))
escapedPEM := strings.ReplaceAll(strings.TrimSpace(privatePEM), "\n", `\n`)
_, err = NewPasswordCipher(escapedPEM, "escaped")
require.NoError(t, err)
}
func TestNormalizePrivateKeyPEMPreservesValidPEM(t *testing.T) {
t.Parallel()
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
privatePEM := string(pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: mustMarshalPKCS8PrivateKey(t, key),
}))
require.Equal(t, strings.TrimSpace(privatePEM), strings.TrimSpace(normalizePrivateKeyPEM(privatePEM)))
}
func mustMarshalPKCS8PrivateKey(t *testing.T, key *rsa.PrivateKey) []byte {
t.Helper()
der, err := x509.MarshalPKCS8PrivateKey(key)
require.NoError(t, err)
return der
}