security: load Creator secrets from files
This commit was merged in pull request #1.
This commit is contained in:
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
@@ -27,8 +28,20 @@ func main() {
|
||||
if addr == "" {
|
||||
addr = ":8080"
|
||||
}
|
||||
bootstrapToken, sessionKey := os.Getenv("CREATOR_BOOTSTRAP_TOKEN"), []byte(os.Getenv("CREATOR_SESSION_SIGNING_KEY"))
|
||||
creatorUsername, creatorPassword := os.Getenv("CREATOR_USERNAME"), os.Getenv("CREATOR_PASSWORD")
|
||||
bootstrapToken, err := readRequiredSecret("CREATOR_BOOTSTRAP_TOKEN")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
sessionKeyValue, err := readRequiredSecret("CREATOR_SESSION_SIGNING_KEY")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
creatorPassword, err := readRequiredSecret("CREATOR_PASSWORD")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
sessionKey := []byte(sessionKeyValue)
|
||||
creatorUsername := os.Getenv("CREATOR_USERNAME")
|
||||
repository, err := postgres.OpenSituation(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("POSTGRES_USER"), Password: os.Getenv("POSTGRES_PASSWORD")})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -299,3 +312,28 @@ func bearerToken(r *http.Request) string {
|
||||
}
|
||||
return strings.TrimPrefix(value, "Bearer ")
|
||||
}
|
||||
|
||||
func readRequiredSecret(name string) (string, error) {
|
||||
direct, directSet := os.LookupEnv(name)
|
||||
fileName, fileSet := os.LookupEnv(name + "_FILE")
|
||||
if directSet && fileSet {
|
||||
return "", fmt.Errorf("%s and %s_FILE cannot both be configured", name, name)
|
||||
}
|
||||
if fileSet {
|
||||
if fileName == "" {
|
||||
return "", fmt.Errorf("%s_FILE is empty", name)
|
||||
}
|
||||
contents, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read %s_FILE: %w", name, err)
|
||||
}
|
||||
direct = strings.TrimRight(string(contents), "\r\n")
|
||||
if strings.ContainsAny(direct, "\r\n") {
|
||||
return "", fmt.Errorf("%s_FILE must contain one line", name)
|
||||
}
|
||||
}
|
||||
if direct == "" {
|
||||
return "", fmt.Errorf("%s is required", name)
|
||||
}
|
||||
return direct, nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -67,3 +69,59 @@ func TestCreatorSessionHandlerPreservesBootstrapRecovery(t *testing.T) {
|
||||
t.Fatalf("bootstrap recovery status = %d", response.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRequiredSecretFromEnvironment(t *testing.T) {
|
||||
t.Setenv("TEST_CREATOR_SECRET", "direct-value")
|
||||
value, err := readRequiredSecret("TEST_CREATOR_SECRET")
|
||||
if err != nil || value != "direct-value" {
|
||||
t.Fatalf("value=%q err=%v", value, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRequiredSecretFromFile(t *testing.T) {
|
||||
fileName := filepath.Join(t.TempDir(), "secret")
|
||||
if err := os.WriteFile(fileName, []byte("file-value\r\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("TEST_CREATOR_SECRET_FILE", fileName)
|
||||
value, err := readRequiredSecret("TEST_CREATOR_SECRET")
|
||||
if err != nil || value != "file-value" {
|
||||
t.Fatalf("value=%q err=%v", value, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRequiredSecretFailsClosed(t *testing.T) {
|
||||
t.Run("both sources", func(t *testing.T) {
|
||||
t.Setenv("TEST_CREATOR_SECRET", "direct")
|
||||
t.Setenv("TEST_CREATOR_SECRET_FILE", filepath.Join(t.TempDir(), "secret"))
|
||||
if _, err := readRequiredSecret("TEST_CREATOR_SECRET"); err == nil {
|
||||
t.Fatal("expected ambiguous configuration to fail")
|
||||
}
|
||||
})
|
||||
t.Run("empty file", func(t *testing.T) {
|
||||
fileName := filepath.Join(t.TempDir(), "secret")
|
||||
if err := os.WriteFile(fileName, nil, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("TEST_CREATOR_SECRET_FILE", fileName)
|
||||
if _, err := readRequiredSecret("TEST_CREATOR_SECRET"); err == nil {
|
||||
t.Fatal("expected empty secret to fail")
|
||||
}
|
||||
})
|
||||
t.Run("multiline file", func(t *testing.T) {
|
||||
fileName := filepath.Join(t.TempDir(), "secret")
|
||||
if err := os.WriteFile(fileName, []byte("first\nsecond\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("TEST_CREATOR_SECRET_FILE", fileName)
|
||||
if _, err := readRequiredSecret("TEST_CREATOR_SECRET"); err == nil {
|
||||
t.Fatal("expected multiline secret to fail")
|
||||
}
|
||||
})
|
||||
t.Run("missing file", func(t *testing.T) {
|
||||
t.Setenv("TEST_CREATOR_SECRET_FILE", filepath.Join(t.TempDir(), "missing"))
|
||||
if _, err := readRequiredSecret("TEST_CREATOR_SECRET"); err == nil {
|
||||
t.Fatal("expected missing secret file to fail")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user