feat(server): activate projected authentication safely

This commit is contained in:
User
2026-08-22 01:01:36 +02:00
parent 903c0b4de5
commit 3d9a9f0675
32 changed files with 1837 additions and 38 deletions
+84 -3
View File
@@ -39,7 +39,14 @@ type descriptor struct {
}
type authenticationDescriptor struct {
ConfigDirectory string `yaml:"configDirectory"`
ConfigDirectory string `yaml:"configDirectory"`
RuntimeProjection *runtimeProjectionDescriptor `yaml:"runtimeProjection"`
}
type runtimeProjectionDescriptor struct {
Directory string `yaml:"directory"`
UID uint32 `yaml:"uid"`
GID uint32 `yaml:"gid"`
}
type workspaceRepositoryDescriptor struct {
@@ -55,9 +62,17 @@ type WorkspaceRepository struct {
Access string
}
// RuntimeProjection is the non-secret runtime root and numeric container ownership contract.
type RuntimeProjection struct {
Directory string
UID uint32
GID uint32
}
// Authentication is the non-secret filesystem location for the installation auth configuration.
type Authentication struct {
ConfigDirectory string
ConfigDirectory string
RuntimeProjection *RuntimeProjection
}
// Installation is a validated local Compose installation. It intentionally contains paths, not
@@ -114,6 +129,14 @@ func Load(path string) (Installation, error) {
return Installation{}, errors.New("authentication.configDirectory must be an absolute canonical path")
}
authentication := Authentication{ConfigDirectory: raw.Authentication.ConfigDirectory}
if raw.Authentication.RuntimeProjection != nil {
authentication.RuntimeProjection = &RuntimeProjection{
Directory: raw.Authentication.RuntimeProjection.Directory,
UID: raw.Authentication.RuntimeProjection.UID,
GID: raw.Authentication.RuntimeProjection.GID,
}
}
installation := Installation{
Path: path,
Profile: raw.Profile,
@@ -124,7 +147,7 @@ func Load(path string) (Installation, error) {
Branch: raw.WorkspaceRepository.Branch,
Access: raw.WorkspaceRepository.Access,
},
Authentication: Authentication{ConfigDirectory: raw.Authentication.ConfigDirectory},
Authentication: authentication,
Overrides: make([]string, 0, len(raw.Overrides)),
}
values, err := installation.environmentValues()
@@ -140,6 +163,14 @@ func Load(path string) (Installation, error) {
}
installation.Overrides = append(installation.Overrides, filepath.Clean(override))
}
if err := installation.validateRuntimeAuthProjection(values); err != nil {
return Installation{}, err
}
if installation.HasRuntimeAuthProjection() {
if err := requireRegularFile(installation.runtimeAuthProjectionComposePath(), "runtime authentication Compose override"); err != nil {
return Installation{}, err
}
}
for _, composeFile := range installation.ComposeFiles()[:2] {
if err := requireRegularFile(composeFile, "Compose file"); err != nil {
return Installation{}, err
@@ -161,6 +192,49 @@ func Load(path string) (Installation, error) {
// AuthenticationDirectory returns the descriptor-owned, non-secret authentication root.
func (i Installation) AuthenticationDirectory() string { return i.Authentication.ConfigDirectory }
// RuntimeAuthProjection returns an independent descriptor copy when this installation uses the
// Linux-only runtime auth publication contract.
func (i Installation) RuntimeAuthProjection() *RuntimeProjection {
if i.Authentication.RuntimeProjection == nil {
return nil
}
projection := *i.Authentication.RuntimeProjection
return &projection
}
// HasRuntimeAuthProjection reports whether the descriptor selects the runtime auth projection.
func (i Installation) HasRuntimeAuthProjection() bool {
return i.Authentication.RuntimeProjection != nil
}
func (i Installation) validateRuntimeAuthProjection(values map[string]string) error {
projection := i.RuntimeAuthProjection()
if projection == nil {
if values["THT_AUTH_RUNTIME_ROOT"] != "" {
return errors.New("THT_AUTH_RUNTIME_ROOT requires authentication.runtimeProjection")
}
return nil
}
if i.Profile != "server" {
return errors.New("authentication.runtimeProjection requires the server profile")
}
if err := safeio.ValidateCanonicalPath(projection.Directory); err != nil || projection.Directory == i.AuthenticationDirectory() {
return errors.New("authentication.runtimeProjection.directory must be a distinct absolute canonical path")
}
if projection.UID != 10001 || projection.GID != 10001 {
return errors.New("authentication.runtimeProjection requires uid and gid 10001")
}
if values["THT_AUTH_RUNTIME_ROOT"] != projection.Directory {
return errors.New("authentication.runtimeProjection.directory must match THT_AUTH_RUNTIME_ROOT")
}
for _, override := range i.Overrides {
if filepath.Clean(override) == i.runtimeAuthProjectionComposePath() {
return errors.New("runtime authentication Compose override is automatic and must not be declared")
}
}
return nil
}
var safeGitBranch = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/-]*$`)
var scpSSHRemote = regexp.MustCompile(`^git@[^:/\s]+:[^\s]+$`)
@@ -250,6 +324,9 @@ func (i Installation) ComposeFiles() []string {
filepath.Join(i.ProjectDirectory, "deploy", "compose."+i.Profile+".yaml"),
}
files = append(files, i.Overrides...)
if i.HasRuntimeAuthProjection() {
files = append(files, i.runtimeAuthProjectionComposePath())
}
currentImage := i.CurrentImageOverridePath()
if info, err := os.Lstat(currentImage); err == nil && info.Mode().IsRegular() {
files = append(files, currentImage)
@@ -257,6 +334,10 @@ func (i Installation) ComposeFiles() []string {
return files
}
func (i Installation) runtimeAuthProjectionComposePath() string {
return filepath.Join(i.ProjectDirectory, "deploy", "compose.auth-runtime-projection.yaml")
}
// ControlDirectory contains state that is private to one installation descriptor, even when
// multiple installations intentionally share one source checkout.
func (i Installation) ControlDirectory() string {
@@ -52,6 +52,104 @@ func TestLoadSelectsServerComposeFiles(t *testing.T) {
assertStringsEqual(t, installation.ComposeFiles(), want)
}
func TestLoadAcceptsServerRuntimeProjectionAndPlacesAutomaticOverrideBeforeCurrentImage(t *testing.T) {
installationPath, projectDirectory, envFile, override := writeInstallation(t, "server")
root := filepath.Dir(installationPath)
authDirectory := filepath.Join(root, "canonical-auth")
runtimeDirectory := filepath.Join(root, "runtime-auth")
automaticOverride := filepath.Join(projectDirectory, "deploy", "compose.auth-runtime-projection.yaml")
if err := os.WriteFile(automaticOverride, []byte("services: {}\n"), 0o600); err != nil {
t.Fatal(err)
}
candidate := Installation{Path: installationPath, ProjectDirectory: projectDirectory}
currentImage := candidate.CurrentImageOverridePath()
if err := os.MkdirAll(filepath.Dir(currentImage), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(currentImage, []byte("services: {}\n"), 0o600); err != nil {
t.Fatal(err)
}
writeRuntimeProjectionFixture(t, installationPath, envFile, "server", authDirectory, runtimeDirectory, runtimeDirectory, 10001, 10001, []string{override})
installation, err := Load(installationPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if !installation.HasRuntimeAuthProjection() {
t.Fatal("HasRuntimeAuthProjection() = false, want true")
}
projection := installation.RuntimeAuthProjection()
if projection == nil || projection.Directory != runtimeDirectory || projection.UID != 10001 || projection.GID != 10001 {
t.Fatalf("RuntimeAuthProjection() = %#v", projection)
}
projection.Directory = "mutated"
if got := installation.RuntimeAuthProjection(); got == nil || got.Directory != runtimeDirectory {
t.Fatalf("RuntimeAuthProjection() did not return an independent copy: %#v", got)
}
want := []string{
filepath.Join(projectDirectory, "compose.yaml"),
filepath.Join(projectDirectory, "deploy", "compose.server.yaml"),
override,
automaticOverride,
currentImage,
}
assertStringsEqual(t, installation.ComposeFiles(), want)
}
func TestLoadRejectsInvalidRuntimeProjection(t *testing.T) {
for _, test := range []struct {
name string
profile string
configDirectory func(root string) string
runtimeDirectory func(root string) string
environmentRoot func(root string) string
uid, gid uint32
manualOverride bool
}{
{name: "local profile", profile: "local", configDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, runtimeDirectory: func(root string) string { return filepath.Join(root, "runtime-auth") }, environmentRoot: func(root string) string { return filepath.Join(root, "runtime-auth") }, uid: 10001, gid: 10001},
{name: "relative runtime directory", profile: "server", configDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, runtimeDirectory: func(string) string { return "relative-runtime-auth" }, environmentRoot: func(string) string { return "relative-runtime-auth" }, uid: 10001, gid: 10001},
{name: "noncanonical runtime directory", profile: "server", configDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, runtimeDirectory: func(root string) string { return root + "/runtime-auth/../runtime-auth" }, environmentRoot: func(root string) string { return filepath.Join(root, "runtime-auth") }, uid: 10001, gid: 10001},
{name: "equal canonical and runtime directories", profile: "server", configDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, runtimeDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, environmentRoot: func(root string) string { return filepath.Join(root, "canonical-auth") }, uid: 10001, gid: 10001},
{name: "uid mismatch", profile: "server", configDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, runtimeDirectory: func(root string) string { return filepath.Join(root, "runtime-auth") }, environmentRoot: func(root string) string { return filepath.Join(root, "runtime-auth") }, uid: 10000, gid: 10001},
{name: "gid mismatch", profile: "server", configDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, runtimeDirectory: func(root string) string { return filepath.Join(root, "runtime-auth") }, environmentRoot: func(root string) string { return filepath.Join(root, "runtime-auth") }, uid: 10001, gid: 10000},
{name: "missing runtime environment", profile: "server", configDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, runtimeDirectory: func(root string) string { return filepath.Join(root, "runtime-auth") }, environmentRoot: func(root string) string { return "" }, uid: 10001, gid: 10001},
{name: "mismatched runtime environment", profile: "server", configDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, runtimeDirectory: func(root string) string { return filepath.Join(root, "runtime-auth") }, environmentRoot: func(root string) string { return filepath.Join(root, "different-runtime-auth") }, uid: 10001, gid: 10001},
{name: "manual automatic override", profile: "server", configDirectory: func(root string) string { return filepath.Join(root, "canonical-auth") }, runtimeDirectory: func(root string) string { return filepath.Join(root, "runtime-auth") }, environmentRoot: func(root string) string { return filepath.Join(root, "runtime-auth") }, uid: 10001, gid: 10001, manualOverride: true},
} {
t.Run(test.name, func(t *testing.T) {
installationPath, projectDirectory, envFile, override := writeInstallation(t, test.profile)
root := filepath.Dir(installationPath)
automaticOverride := filepath.Join(projectDirectory, "deploy", "compose.auth-runtime-projection.yaml")
if err := os.WriteFile(automaticOverride, []byte("services: {}\n"), 0o600); err != nil {
t.Fatal(err)
}
overrides := []string{override}
if test.manualOverride {
overrides = append(overrides, automaticOverride)
}
writeRuntimeProjectionFixture(t, installationPath, envFile, test.profile, test.configDirectory(root), test.runtimeDirectory(root), test.environmentRoot(root), test.uid, test.gid, overrides)
if _, err := Load(installationPath); err == nil {
t.Fatal("Load() unexpectedly accepted an invalid runtime authentication projection")
}
})
}
}
func TestLoadRejectsRuntimeProjectionEnvironmentWithoutDescriptor(t *testing.T) {
installationPath, _, envFile, _ := writeInstallation(t, "server")
authDirectory := filepath.Join(filepath.Dir(installationPath), "auth")
runtimeDirectory := filepath.Join(filepath.Dir(installationPath), "runtime-auth")
contents := "THT_AUTH_CONFIG_ROOT=" + strconv.Quote(authDirectory) + "\nTHT_AUTH_RUNTIME_ROOT=" + strconv.Quote(runtimeDirectory) + "\n"
if err := os.WriteFile(envFile, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
if _, err := Load(installationPath); err == nil {
t.Fatal("Load() unexpectedly accepted THT_AUTH_RUNTIME_ROOT without runtimeProjection")
}
}
func TestLoadRequiresAndReturnsTypedWorkspaceRepositoryForGitInstallations(t *testing.T) {
installationPath, projectDirectory, envFile, _ := writeInstallation(t, "local")
gitOverride := filepath.Join(projectDirectory, "deploy", "compose.git-ssh.yaml")
@@ -280,6 +378,25 @@ func TestParseAuthenticationDirectoryEnvironment(t *testing.T) {
}
}
func writeRuntimeProjectionFixture(t *testing.T, installationPath, envFile, profile, configDirectory, runtimeDirectory, environmentRoot string, uid, gid uint32, overrides []string) {
t.Helper()
lines := []string{"THT_AUTH_CONFIG_ROOT=" + strconv.Quote(configDirectory)}
if environmentRoot != "" {
lines = append(lines, "THT_AUTH_RUNTIME_ROOT="+strconv.Quote(environmentRoot))
}
if err := os.WriteFile(envFile, []byte(strings.Join(lines, "\n")+"\n"), 0o600); err != nil {
t.Fatal(err)
}
projectDirectory := filepath.Join(filepath.Dir(installationPath), "project directory with spaces")
contents := "profile: " + profile + "\nprojectDirectory: " + projectDirectory + "\nenvFile: " + envFile + "\nauthentication:\n configDirectory: " + configDirectory + "\n runtimeProjection:\n directory: " + runtimeDirectory + "\n uid: " + strconv.FormatUint(uint64(uid), 10) + "\n gid: " + strconv.FormatUint(uint64(gid), 10) + "\noverrides:\n"
for _, override := range overrides {
contents += " - " + override + "\n"
}
if err := os.WriteFile(installationPath, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
}
func writeInstallation(t *testing.T, profile string) (string, string, string, string) {
t.Helper()