fix(deploy): remove initializer and self-bootstrap secrets

This commit is contained in:
2026-08-11 00:46:54 +02:00
parent 3ae12367e3
commit b40f4e2e1b
14 changed files with 263 additions and 141 deletions
+4 -2
View File
@@ -3,5 +3,7 @@ DOGAMA_VERSION=latest
DOGAMA_HTTP_PORT=8080
TZ=UTC
DOGAMA_DATA_PATH=./data
DOGAMA_SERVERS_PATH=./data/servers
DOGAMA_BACKUPS_PATH=./data/backups
DOGAMA_SERVERS_PATH=./servers
DOGAMA_BACKUPS_PATH=./backups
DOGAMA_NETWORK=dogama
DOGAMA_GAMES_NETWORK=dogama-games
+2 -5
View File
@@ -12,14 +12,11 @@ RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -buildvcs=false \
-ldflags="-s -w -X main.version=$VERSION -X main.commit=$COMMIT" -o /out/dogama ./cmd/dogama && \
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -buildvcs=false \
-ldflags="-s -w -X main.version=$VERSION -X main.commit=$COMMIT" -o /out/dogama-agent ./cmd/dogama-agent && \
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -buildvcs=false \
-ldflags="-s -w" -o /out/dogama-init ./cmd/dogama-init
-ldflags="-s -w -X main.version=$VERSION -X main.commit=$COMMIT" -o /out/dogama-agent ./cmd/dogama-agent
FROM gcr.io/distroless/static-debian12:nonroot@sha256:f5b485ea962d9bd1186b2f6b3a061191539b905b82ec395de78cbfae51f20e35 AS dogama
FROM gcr.io/distroless/static-debian12:latest@sha256:a9fcaedd4c9b59e12dd65d954f0b5044f19b0647a8a3712e77205df9e7b102cd AS dogama
WORKDIR /var/lib/dogama
COPY --from=build /out/dogama /usr/local/bin/dogama
COPY --from=build /out/dogama-init /usr/local/bin/dogama-init
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/dogama"]
-52
View File
@@ -1,52 +0,0 @@
// Command dogama-init initializes persistent secrets for the standard Compose deployment.
package main
import (
"fmt"
"os"
"strconv"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/internalsecrets"
)
func main() {
uid, gid := integer("DOGAMA_APP_UID", 65532), integer("DOGAMA_APP_GID", 65532)
items := []struct {
path string
uid, gid int
mode os.FileMode
}{
{"/var/lib/dogama-agent/secrets/token", 0, gid, 0o640},
{"/var/lib/dogama/secrets/master_key", uid, gid, 0o600},
}
for _, item := range items {
if _, err := internalsecrets.Ensure(item.path, item.uid, item.gid, item.mode); err != nil {
fmt.Fprintf(os.Stderr, "internal secret initialization failed: %v\n", err)
os.Exit(1)
}
}
for _, path := range []string{"/var/lib/dogama", "/srv/game-servers", "/srv/game-backups"} {
if err := prepareDirectory(path, uid, gid); err != nil {
fmt.Fprintf(os.Stderr, "persistent directory initialization failed: %v\n", err)
os.Exit(1)
}
}
}
func prepareDirectory(path string, uid, gid int) error {
if err := os.MkdirAll(path, 0o750); err != nil {
return err
}
if err := os.Chmod(path, 0o750); err != nil {
return err
}
return os.Chown(path, uid, gid)
}
func integer(name string, fallback int) int {
value, err := strconv.Atoi(os.Getenv(name))
if err != nil || value < 0 {
return fallback
}
return value
}
+49 -13
View File
@@ -67,15 +67,10 @@ func run(logger *slog.Logger) error {
auditService := audit.New(db)
var notificationService *notification.Service
keyFile := environment("DOGAMA_MASTER_KEY_FILE", "secrets/master_key")
if keyFile == "secrets/master_key" {
if _, keyErr := internalsecrets.Ensure(keyFile, os.Getuid(), os.Getgid(), 0o600); keyErr != nil {
return errors.New("initialize encryption key file")
}
}
if keyFile != "" {
key, keyErr := os.ReadFile(keyFile)
key, keyErr := loadMasterKey(keyFile)
if keyErr != nil {
return errors.New("read encryption key file")
return keyErr
}
notificationService, keyErr = notification.New(db, key)
if keyErr != nil {
@@ -97,12 +92,7 @@ func run(logger *slog.Logger) error {
if agentURL == "" || tokenFile == "" {
return errors.New("DOGAMA_AGENT_URL and DOGAMA_AGENT_TOKEN_FILE must be configured together")
}
secret, readErr := os.ReadFile(tokenFile)
if readErr != nil {
return errors.New("read agent token file")
}
secret = bytes.TrimSuffix(bytes.TrimSuffix(secret, []byte("\n")), []byte("\r"))
agent, clientErr := agentclient.New(agentURL, secret, &http.Client{Timeout: 15 * time.Minute})
agent, clientErr := waitForAgent(ctx, agentURL, tokenFile, &http.Client{Timeout: 15 * time.Minute}, 60*time.Second, 250*time.Millisecond)
if clientErr != nil {
return clientErr
}
@@ -160,6 +150,52 @@ func run(logger *slog.Logger) error {
}
}
func loadMasterKey(path string) ([]byte, error) {
if path == "secrets/master_key" {
if _, err := internalsecrets.Ensure(path, 0o600); err != nil {
return nil, errors.New("initialize encryption key file")
}
}
key, err := os.ReadFile(path)
if err != nil {
return nil, errors.New("read encryption key file")
}
return key, nil
}
func waitForAgent(ctx context.Context, agentURL, tokenFile string, httpClient *http.Client, timeout, retryInterval time.Duration) (*agentclient.Client, error) {
waitCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
for {
secret, err := os.ReadFile(tokenFile)
if err == nil {
secret = bytes.TrimSuffix(bytes.TrimSuffix(secret, []byte("\n")), []byte("\r"))
if len(secret) != 32 {
return nil, errors.New("agent token file is invalid")
}
client, clientErr := agentclient.New(agentURL, secret, httpClient)
if clientErr != nil {
return nil, errors.New("agent configuration is invalid")
}
healthCtx, healthCancel := context.WithTimeout(waitCtx, 2*time.Second)
healthErr := client.Health(healthCtx)
healthCancel()
if healthErr == nil {
return client, nil
}
} else if !errors.Is(err, os.ErrNotExist) {
return nil, errors.New("read agent token file")
}
timer := time.NewTimer(retryInterval)
select {
case <-waitCtx.Done():
timer.Stop()
return nil, errors.New("agent did not become ready before startup timeout")
case <-timer.C:
}
}
}
func runObservabilityScheduler(ctx context.Context, auditService *audit.Service, notificationService *notification.Service, logger *slog.Logger) {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
+94
View File
@@ -0,0 +1,94 @@
package main
import (
"bytes"
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
)
func TestLoadMasterKeyCreatesAndReusesDefault(t *testing.T) {
working := t.TempDir()
previous, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(working); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Chdir(previous) })
first, err := loadMasterKey("secrets/master_key")
if err != nil {
t.Fatal(err)
}
second, err := loadMasterKey("secrets/master_key")
if err != nil {
t.Fatal(err)
}
if len(first) != 32 || !bytes.Equal(first, second) {
t.Fatal("master key was not created once and reused")
}
}
func TestLoadMasterKeyRefusesInvalidExistingDefault(t *testing.T) {
working := t.TempDir()
previous, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(working); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Chdir(previous) })
if err := os.Mkdir("secrets", 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join("secrets", "master_key"), []byte("invalid"), 0o600); err != nil {
t.Fatal(err)
}
if _, err := loadMasterKey("secrets/master_key"); err == nil {
t.Fatal("invalid master key unexpectedly accepted")
}
got, _ := os.ReadFile(filepath.Join("secrets", "master_key"))
if string(got) != "invalid" {
t.Fatal("invalid master key was replaced")
}
}
func TestWaitForAgentHandlesConcurrentFirstStart(t *testing.T) {
ready := make(chan struct{})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
select {
case <-ready:
w.WriteHeader(http.StatusOK)
default:
http.Error(w, "not ready", http.StatusServiceUnavailable)
}
}))
t.Cleanup(server.Close)
tokenFile := filepath.Join(t.TempDir(), "secrets", "token")
go func() {
time.Sleep(20 * time.Millisecond)
_ = os.MkdirAll(filepath.Dir(tokenFile), 0o750)
_ = os.WriteFile(tokenFile, bytes.Repeat([]byte{0x31}, 32), 0o640)
close(ready)
}()
if _, err := waitForAgent(context.Background(), server.URL, tokenFile, server.Client(), time.Second, 5*time.Millisecond); err != nil {
t.Fatal(err)
}
}
func TestWaitForAgentRefusesInvalidToken(t *testing.T) {
tokenFile := filepath.Join(t.TempDir(), "token")
if err := os.WriteFile(tokenFile, []byte("invalid"), 0o640); err != nil {
t.Fatal(err)
}
if _, err := waitForAgent(context.Background(), "http://agent:8081", tokenFile, http.DefaultClient, time.Second, time.Millisecond); err == nil {
t.Fatal("invalid agent token unexpectedly accepted")
}
}
+9 -33
View File
@@ -1,23 +1,4 @@
services:
init:
image: git.zaynet.fr/dogama/dogama:${DOGAMA_VERSION:-latest}
user: "0:0"
read_only: true
entrypoint: ["/usr/local/bin/dogama-init"]
volumes:
- ${DOGAMA_DATA_PATH:-./data}:/var/lib/dogama
- agent_state:/var/lib/dogama-agent
- ${DOGAMA_SERVERS_PATH:-./data/servers}:/srv/game-servers
- ${DOGAMA_BACKUPS_PATH:-./data/backups}:/srv/game-backups
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- CHOWN
- DAC_READ_SEARCH
- FOWNER
dogama:
image: git.zaynet.fr/dogama/dogama:${DOGAMA_VERSION:-latest}
restart: unless-stopped
@@ -30,8 +11,8 @@ services:
volumes:
- ${DOGAMA_DATA_PATH:-./data}:/var/lib/dogama
- agent_state:/var/lib/dogama-agent:ro
- ${DOGAMA_SERVERS_PATH:-./data/servers}:/srv/game-servers
- ${DOGAMA_BACKUPS_PATH:-./data/backups}:/srv/game-backups
- ${DOGAMA_SERVERS_PATH:-./servers}:/srv/game-servers
- ${DOGAMA_BACKUPS_PATH:-./backups}:/srv/game-backups
tmpfs:
- /tmp:rw,noexec,nosuid,nodev,size=32m
security_opt:
@@ -39,12 +20,9 @@ services:
cap_drop:
- ALL
networks:
- frontend
- dogama
- control
- games
depends_on:
init:
condition: service_completed_successfully
agent:
condition: service_started
@@ -54,11 +32,12 @@ services:
read_only: true
environment:
TZ: ${TZ:-UTC}
DOGAMA_DOCKER_NETWORK: ${DOGAMA_GAMES_NETWORK:-dogama-games}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- agent_state:/var/lib/dogama-agent
- ${DOGAMA_SERVERS_PATH:-./data/servers}:/srv/game-servers
- ${DOGAMA_BACKUPS_PATH:-./data/backups}:/srv/game-backups
- ${DOGAMA_SERVERS_PATH:-./servers}:/srv/game-servers
- ${DOGAMA_BACKUPS_PATH:-./backups}:/srv/game-backups
tmpfs:
- /tmp:rw,noexec,nosuid,nodev,size=16m
security_opt:
@@ -67,17 +46,14 @@ services:
- ALL
networks:
- control
- games
depends_on:
init:
condition: service_completed_successfully
networks:
frontend:
dogama:
name: ${DOGAMA_NETWORK:-dogama}
control:
internal: true
games:
name: dogama-games
name: ${DOGAMA_GAMES_NETWORK:-dogama-games}
volumes:
agent_state:
+5
View File
@@ -7,6 +7,8 @@ import (
"os"
"path/filepath"
"regexp"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/internalsecrets"
)
// Config contains bootstrap-only settings for the restricted agent.
@@ -22,6 +24,9 @@ type Config struct {
// LoadConfig reads the agent's bootstrap settings and shared secret file.
func LoadConfig() (Config, error) {
tokenFile := environment("DOGAMA_AGENT_TOKEN_FILE", "/var/lib/dogama-agent/secrets/token")
if _, err := internalsecrets.Ensure(tokenFile, 0o640); err != nil {
return Config{}, errors.New("initialize agent secret")
}
secret, err := readSecretFile(tokenFile)
if err != nil {
return Config{}, err
+38 -5
View File
@@ -11,7 +11,7 @@ func TestLoadConfigReadsSecretFileAndRoots(t *testing.T) {
temporary := t.TempDir()
secretPath := filepath.Join(temporary, "agent-token")
secret := bytes.Repeat([]byte("a"), 32)
if err := os.WriteFile(secretPath, append(secret, '\n'), 0o600); err != nil {
if err := os.WriteFile(secretPath, secret, 0o640); err != nil {
t.Fatal(err)
}
serverRoot := filepath.Join(temporary, "servers")
@@ -23,20 +23,53 @@ func TestLoadConfigReadsSecretFileAndRoots(t *testing.T) {
t.Setenv("DOGAMA_ALLOWED_BACKUP_ROOT", "")
t.Setenv("DOGAMA_AGENT_REGISTRY_PATH", filepath.Join(temporary, "registry.json"))
t.Setenv("DOGAMA_DOCKER_SOCKET", filepath.Join(temporary, "docker.sock"))
t.Setenv("DOGAMA_DOCKER_NETWORK", "nas-games")
config, err := LoadConfig()
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(config.Secret, secret) || len(config.AllowedRoots) != 2 || config.ListenAddress != ":8081" || config.DockerNetwork != "dogama-games" {
if !bytes.Equal(config.Secret, secret) || len(config.AllowedRoots) != 2 || config.ListenAddress != ":8081" || config.DockerNetwork != "nas-games" {
t.Fatalf("config = %#v", config)
}
}
func TestLoadConfigUsesConfinedProductionDefaults(t *testing.T) {
t.Setenv("DOGAMA_AGENT_TOKEN_FILE", filepath.Join(t.TempDir(), "missing-token"))
func TestLoadConfigCreatesAndReusesToken(t *testing.T) {
tokenPath := filepath.Join(t.TempDir(), "secrets", "token")
t.Setenv("DOGAMA_AGENT_TOKEN_FILE", tokenPath)
t.Setenv("DOGAMA_ALLOWED_SERVER_ROOT", "")
t.Setenv("DOGAMA_ALLOWED_BACKUP_ROOT", "")
first, err := LoadConfig()
if err != nil {
t.Fatal(err)
}
second, err := LoadConfig()
if err != nil {
t.Fatal(err)
}
if len(first.Secret) != 32 || !bytes.Equal(first.Secret, second.Secret) {
t.Fatal("agent token was not created once and reused")
}
}
func TestLoadConfigRefusesInvalidExistingToken(t *testing.T) {
tokenPath := filepath.Join(t.TempDir(), "token")
if err := os.WriteFile(tokenPath, []byte("invalid"), 0o640); err != nil {
t.Fatal(err)
}
t.Setenv("DOGAMA_AGENT_TOKEN_FILE", tokenPath)
if _, err := LoadConfig(); err == nil {
t.Fatal("missing default token unexpectedly accepted")
t.Fatal("invalid agent token unexpectedly accepted")
}
got, _ := os.ReadFile(tokenPath)
if string(got) != "invalid" {
t.Fatal("invalid agent token was replaced")
}
}
func TestLoadConfigRefusesInvalidDockerNetwork(t *testing.T) {
t.Setenv("DOGAMA_AGENT_TOKEN_FILE", filepath.Join(t.TempDir(), "secrets", "token"))
t.Setenv("DOGAMA_DOCKER_NETWORK", "host/network")
if _, err := LoadConfig(); err == nil {
t.Fatal("invalid Docker network unexpectedly accepted")
}
}
+17 -13
View File
@@ -43,7 +43,7 @@ func TestDockerRuntimeCreatesFixedSecurityBaseline(t *testing.T) {
if err != nil {
t.Fatal(err)
}
createdBodies := make(chan []byte, 1)
createdBodies := make(chan []byte, 2)
server := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v1.41/images/create":
@@ -62,7 +62,7 @@ func TestDockerRuntimeCreatesFixedSecurityBaseline(t *testing.T) {
})}
go func() { _ = server.Serve(listener) }()
t.Cleanup(func() { _ = server.Shutdown(context.Background()) })
runtime, err := NewDockerRuntime(socket, "dogama-games")
runtime, err := NewDockerRuntime(socket, "nas-games")
if err != nil {
t.Fatal(err)
}
@@ -74,12 +74,14 @@ func TestDockerRuntimeCreatesFixedSecurityBaseline(t *testing.T) {
if err := runtime.CheckPorts(context.Background(), plan.Ports); err != nil {
t.Fatal(err)
}
id, err := runtime.Create(context.Background(), plan, []AssetMount{{HostPath: "/srv/games/.dogama/helper", ContainerPath: "/pal/helper.sh"}})
if err != nil {
t.Fatal(err)
}
if id != "container-1" {
t.Fatalf("container ID = %q", id)
for range 2 {
id, err := runtime.Create(context.Background(), plan, []AssetMount{{HostPath: "/srv/games/.dogama/helper", ContainerPath: "/pal/helper.sh"}})
if err != nil {
t.Fatal(err)
}
if id != "container-1" {
t.Fatalf("container ID = %q", id)
}
}
var payload struct {
User string `json:"User"`
@@ -92,11 +94,13 @@ func TestDockerRuntimeCreatesFixedSecurityBaseline(t *testing.T) {
NanoCPUs int64 `json:"NanoCpus"`
} `json:"HostConfig"`
}
if err := json.Unmarshal(<-createdBodies, &payload); err != nil {
t.Fatal(err)
}
if payload.HostConfig.NetworkMode != "dogama-games" || len(payload.HostConfig.CapDrop) != 1 || payload.HostConfig.CapDrop[0] != "ALL" || len(payload.HostConfig.SecurityOpt) != 1 || payload.HostConfig.Memory <= 0 || payload.HostConfig.NanoCPUs <= 0 {
t.Fatalf("insecure Docker host config: %#v", payload.HostConfig)
for range 2 {
if err := json.Unmarshal(<-createdBodies, &payload); err != nil {
t.Fatal(err)
}
if payload.HostConfig.NetworkMode != "nas-games" || len(payload.HostConfig.CapDrop) != 1 || payload.HostConfig.CapDrop[0] != "ALL" || len(payload.HostConfig.SecurityOpt) != 1 || payload.HostConfig.Memory <= 0 || payload.HostConfig.NanoCPUs <= 0 {
t.Fatalf("insecure Docker host config: %#v", payload.HostConfig)
}
}
if payload.Labels["io.dogama.instance-id"] != plan.InstanceID || payload.Labels["io.dogama.plan-digest"] != plan.PlanDigest {
t.Fatalf("binding labels = %#v", payload.Labels)
+33
View File
@@ -3,6 +3,8 @@ package agent_test
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"io"
"log/slog"
@@ -10,6 +12,7 @@ import (
"net/http/httptest"
"path/filepath"
"testing"
"time"
catalogdata "git.zaynet.fr/DoGaMa/DoGaMa-serv/catalog"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/agent"
@@ -185,6 +188,36 @@ func TestAgentRejectsPlanSubstitutionAndEscapingMount(t *testing.T) {
}
}
func TestAgentRejectsArbitraryNetworkInAPIPlan(t *testing.T) {
root := t.TempDir()
secret := bytes.Repeat([]byte{0x31}, 32)
plan := testPlan(t, "abcdefghijklmnopqrstuvwx", root)
body, err := json.Marshal(plan)
if err != nil {
t.Fatal(err)
}
var payload map[string]any
if err := json.Unmarshal(body, &payload); err != nil {
t.Fatal(err)
}
payload["network"] = "host"
body, err = json.Marshal(payload)
if err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodPost, "/v1/instances", bytes.NewReader(body))
timestamp := time.Now().UTC().Format(time.RFC3339Nano)
nonce := base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{0x42}, 24))
request.Header.Set(agentwire.HeaderTimestamp, timestamp)
request.Header.Set(agentwire.HeaderNonce, nonce)
request.Header.Set("Authorization", agentwire.AuthorizationScheme+" "+agentwire.Signature(secret, request.Method, request.URL.EscapedPath(), timestamp, nonce, body))
recorder := httptest.NewRecorder()
newTestHandler(t, root, secret, fakeDocker{}).ServeHTTP(recorder, request)
if recorder.Code != http.StatusUnprocessableEntity {
t.Fatalf("arbitrary network status = %d, want %d", recorder.Code, http.StatusUnprocessableEntity)
}
}
func testPlan(t *testing.T, instanceID, root string) agentwire.DeploymentPlan {
t.Helper()
snapshots, err := catalog.LoadFS(catalogdata.Files, ".")
+1 -9
View File
@@ -16,7 +16,7 @@ const secretSize = 32
// Ensure creates path atomically with cryptographically random bytes. Existing
// destination files are validated and never replaced.
func Ensure(path string, uid, gid int, mode os.FileMode) (bool, error) {
func Ensure(path string, mode os.FileMode) (bool, error) {
if mode.Perm()&0o007 != 0 || mode.Perm()&0o700 == 0 {
return false, errors.New("secret permissions are invalid")
}
@@ -69,14 +69,6 @@ func Ensure(path string, uid, gid int, mode os.FileMode) (bool, error) {
}
return false, fmt.Errorf("install secret: %w", err)
}
if runtime.GOOS != "windows" && os.Geteuid() == 0 {
if err := os.Chown(path, uid, gid); err != nil {
return false, fmt.Errorf("set secret owner: %w", err)
}
if err := os.Chown(directory, uid, gid); err != nil {
return false, fmt.Errorf("set secret directory owner: %w", err)
}
}
return true, nil
}
+4 -4
View File
@@ -11,7 +11,7 @@ import (
func TestEnsureGeneratesAndReusesSecret(t *testing.T) {
path := filepath.Join(t.TempDir(), "private", "token")
created, err := Ensure(path, os.Getuid(), os.Getgid(), 0o600)
created, err := Ensure(path, 0o600)
if err != nil || !created {
t.Fatalf("first Ensure() = %v, %v", created, err)
}
@@ -19,7 +19,7 @@ func TestEnsureGeneratesAndReusesSecret(t *testing.T) {
if err != nil {
t.Fatal(err)
}
created, err = Ensure(path, os.Getuid(), os.Getgid(), 0o600)
created, err = Ensure(path, 0o600)
if err != nil || created {
t.Fatalf("second Ensure() = %v, %v", created, err)
}
@@ -44,7 +44,7 @@ func TestEnsureConcurrentInitializationCreatesOneSecret(t *testing.T) {
wait.Add(1)
go func() {
defer wait.Done()
wasCreated, err := Ensure(path, os.Getuid(), os.Getgid(), 0o600)
wasCreated, err := Ensure(path, 0o600)
created <- wasCreated
errors <- err
}()
@@ -73,7 +73,7 @@ func TestEnsureRefusesInvalidExistingSecret(t *testing.T) {
if err := os.WriteFile(path, []byte("short"), 0o600); err != nil {
t.Fatal(err)
}
if created, err := Ensure(path, os.Getuid(), os.Getgid(), 0o600); err == nil || created {
if created, err := Ensure(path, 0o600); err == nil || created {
t.Fatalf("invalid existing secret was accepted: %v, %v", created, err)
}
got, _ := os.ReadFile(path)
-2
View File
@@ -23,8 +23,6 @@ for arch in amd64 arm64; do
-ldflags="-s -w -X main.version=$version -X main.commit=$commit" -o "$stage/dogama" ./cmd/dogama
CGO_ENABLED=0 GOOS=linux GOARCH=$arch go build -trimpath -buildvcs=false \
-ldflags="-s -w -X main.version=$version -X main.commit=$commit" -o "$stage/dogama-agent" ./cmd/dogama-agent
CGO_ENABLED=0 GOOS=linux GOARCH=$arch go build -trimpath -buildvcs=false \
-ldflags="-s -w" -o "$stage/dogama-init" ./cmd/dogama-init
go version -m "$stage/dogama" > "$stage/build-info.txt"
tar --sort=name --owner=0 --group=0 --numeric-owner --mtime="@$epoch" \
-C "$release_dir" -czf "$release_dir/dogama-${version}-linux-$arch.tar.gz" "linux-$arch"
+7 -3
View File
@@ -117,7 +117,7 @@ def validate_coverage() -> None:
def validate_compose() -> None:
compose = load_yaml(ROOT / "compose.yaml")
services = compose["services"]
assert set(services) == {"init", "dogama", "agent"}, "Compose must expose only the initializer and required services"
assert set(services) == {"dogama", "agent"}, "Compose must expose exactly the application and restricted agent"
forbidden = {
"DOGAMA_AGENT_TOKEN_FILE", "DOGAMA_MASTER_KEY_FILE", "DOGAMA_SERVERS_ROOT",
"DOGAMA_BACKUPS_ROOT", "DOGAMA_IMPORTS_ROOT", "DOGAMA_ALLOWED_SERVER_ROOT",
@@ -128,14 +128,18 @@ def validate_compose() -> None:
assert forbidden.isdisjoint(environment), f"{name} exposes an internal environment setting"
assert "secrets" not in service, f"{name} still requires a user-provided Compose secret"
assert "secrets" not in compose, "Compose still defines user-provided internal secrets"
assert all(not service.get("cap_add") for service in services.values()), "Compose adds Linux capabilities"
assert "/var/run/docker.sock:/var/run/docker.sock" not in services["dogama"].get("volumes", []), "main application mounts Docker"
assert not services["agent"].get("ports"), "agent must not publish a port"
assert "agent_state:/var/lib/dogama-agent" in services["agent"]["volumes"], "authenticated agent registry is not persistent"
assert "agent_state:/var/lib/dogama-agent:ro" in services["dogama"]["volumes"], "main application cannot read the shared token"
assert all("master_key" not in volume for volume in services["agent"]["volumes"]), "agent can access the master key"
rendered = (ROOT / "compose.yaml").read_text(encoding="utf-8")
assert "${DOGAMA_SERVERS_PATH:-./data/servers}:/srv/game-servers" in rendered
assert "${DOGAMA_BACKUPS_PATH:-./data/backups}:/srv/game-backups" in rendered
assert "${DOGAMA_SERVERS_PATH:-./servers}:/srv/game-servers" in rendered
assert "${DOGAMA_BACKUPS_PATH:-./backups}:/srv/game-backups" in rendered
assert "name: ${DOGAMA_NETWORK:-dogama}" in rendered, "public application network is not configurable"
assert "name: ${DOGAMA_GAMES_NETWORK:-dogama-games}" in rendered, "game network is not configurable"
assert "DOGAMA_DOCKER_NETWORK: ${DOGAMA_GAMES_NETWORK:-dogama-games}" in rendered, "configured game network does not reach the agent"
def validate_workflows() -> None: