package engine import ( "errors" "fmt" "io" "io/fs" "os" "path/filepath" "slices" "strconv" "strings" ) const ( secretsEntryLimit = 1024 secretFileByteLimit = int64(1 << 20) secretsByteLimit = int64(8 << 20) ) // SecretFileStatus describes the outcome for one file considered by // LoadSecretsDir. type SecretFileStatus string const ( SecretFileLoaded SecretFileStatus = "loaded" SecretFileSkippedExisting SecretFileStatus = "skipped_existing" SecretFileNotApplied SecretFileStatus = "not_applied" SecretFileFailed SecretFileStatus = "failed" ) // SecretFileResult is the safe, value-free bootstrap result for one directory // entry. ExistingEmpty is meaningful when Status is SecretFileSkippedExisting. type SecretFileResult struct { Name string Path string Status SecretFileStatus ExistingEmpty bool } // SecretsReport is a detached snapshot of an explicit secrets-directory // bootstrap. It does not retain file contents or environment values. type SecretsReport struct { entries []SecretFileResult } // Entries returns a copy of the report's filename-ordered entries. func (r *SecretsReport) Entries() []SecretFileResult { if r == nil { return nil } return slices.Clone(r.entries) } // WriteTo renders one safe, deterministic line per entry. Names and paths are // quoted so control characters cannot affect terminal output. func (r *SecretsReport) WriteTo(output io.Writer) (int64, error) { if output == nil { return 0, errors.New("configkit: secrets report output is nil") } if r == nil { return 0, nil } var total int64 for _, entry := range r.entries { line := "status=" + strconv.Quote(string(entry.Status)) + " name=" + strconv.Quote(entry.Name) + " path=" + strconv.Quote(entry.Path) + " existing_empty=" + strconv.FormatBool(entry.ExistingEmpty) + "\n" written, err := io.WriteString(output, line) total += int64(written) if err != nil { return total, err } if written != len(line) { return total, io.ErrShortWrite } } return total, nil } // SecretsError reports safe structured diagnostics for a failed explicit // secrets-directory bootstrap. It intentionally does not unwrap filesystem or // environment errors. type SecretsError struct { diagnostics []Diagnostic } // Error implements error without rendering filenames, paths, or file contents. func (e *SecretsError) Error() string { if e == nil { return "configkit: secrets bootstrap failed" } count := 0 for _, diagnostic := range e.diagnostics { if diagnostic.Severity == SeverityError { count++ } } if count == 1 { return "configkit: secrets bootstrap failed with 1 error" } return fmt.Sprintf("configkit: secrets bootstrap failed with %d errors", count) } // Diagnostics returns a deep copy of the bootstrap diagnostics. func (e *SecretsError) Diagnostics() []Diagnostic { if e == nil { return nil } return cloneDiagnostics(e.diagnostics) } type secretsEnvironment struct { lookup func(string) (string, bool) set func(string, string) error } type secretsAccess struct { openDirectory func(string) (*os.File, error) lstat func(string) (fs.FileInfo, error) openFile func(string) (*os.File, error) } type stagedSecret struct { entry int value string } // LoadSecretsDir explicitly loads immediate regular files from directory into // previously unset process environment variables. Applications should call it // once during startup, before concurrent work; Load never calls it. func LoadSecretsDir(directory string) (*SecretsReport, error) { return loadSecretsDir(directory, secretsEnvironment{lookup: os.LookupEnv, set: os.Setenv}, secretsAccess{ openDirectory: os.Open, lstat: os.Lstat, openFile: os.Open, }) } func loadSecretsDir(directory string, environment secretsEnvironment, access secretsAccess) (*SecretsReport, error) { report := &SecretsReport{} if strings.TrimSpace(directory) == "" { return report, secretsFailure(CodeSecretsInvalidPath, "secrets directory path is required") } canonical, err := canonicalExistingPath(directory) if err != nil { return report, secretsFailure(classifySecretsFilesystemError(err), "secrets directory could not be canonicalized: "+strconv.Quote(cleanAbsolutePath(directory))) } info, err := os.Stat(canonical) if err != nil { return report, secretsFailure(classifySecretsFilesystemError(err), "secrets directory could not be inspected: "+strconv.Quote(canonical)) } if !info.IsDir() { return report, secretsFailure(CodeSecretsInvalidPath, "secrets directory is not a directory: "+strconv.Quote(canonical)) } directoryFile, err := access.openDirectory(canonical) if err != nil { return report, secretsFailure(classifySecretsFilesystemError(err), "secrets directory could not be opened: "+strconv.Quote(canonical)) } entries, readErr := readSecretsEntries(directoryFile) closeErr := directoryFile.Close() if readErr != nil { return report, secretsFailure(classifySecretsFilesystemError(readErr), "secrets directory could not be enumerated: "+strconv.Quote(canonical)) } if closeErr != nil { return report, secretsFailure(CodeSecretsRead, "secrets directory could not be closed: "+strconv.Quote(canonical)) } slices.SortFunc(entries, func(left, right fs.DirEntry) int { return strings.Compare(left.Name(), right.Name()) }) report.entries = make([]SecretFileResult, len(entries)) for index, entry := range entries { name := entry.Name() report.entries[index] = SecretFileResult{Name: name, Path: filepath.Join(canonical, name), Status: SecretFileNotApplied} } if len(entries) > secretsEntryLimit { return report, secretsFailure(CodeSecretsLimitExceeded, "secrets directory exceeds the 1024-entry limit: "+strconv.Quote(canonical)) } for index := range report.entries { entry := &report.entries[index] if !validEnvironmentName(entry.Name) { entry.Status = SecretFileFailed return report, secretsFailure(CodeSecretsInvalidName, "secret filename is not a valid environment name: "+strconv.Quote(entry.Path)) } info, err := access.lstat(entry.Path) if err != nil { entry.Status = SecretFileFailed return report, secretsFailure(classifySecretsFilesystemError(err), "secret file could not be inspected: "+strconv.Quote(entry.Path)) } if info.Mode()&fs.ModeSymlink != 0 || !info.Mode().IsRegular() { entry.Status = SecretFileFailed return report, secretsFailure(CodeSecretsNotRegular, "secret entry is not a regular file: "+strconv.Quote(entry.Path)) } } staged := make([]stagedSecret, 0, len(report.entries)) var consumed int64 for index := range report.entries { entry := &report.entries[index] if existing, present := environment.lookup(entry.Name); present { entry.Status = SecretFileSkippedExisting entry.ExistingEmpty = existing == "" continue } remaining := secretsByteLimit - consumed value, used, err := readSecretValue(entry.Path, remaining, access) if err != nil { entry.Status = SecretFileFailed return report, secretsFailure(secretReadCode(err), secretReadMessage(err)+": "+strconv.Quote(entry.Path)) } consumed += used staged = append(staged, stagedSecret{entry: index, value: value}) } for _, secret := range staged { entry := &report.entries[secret.entry] if existing, present := environment.lookup(entry.Name); present { entry.Status = SecretFileSkippedExisting entry.ExistingEmpty = existing == "" continue } if err := environment.set(entry.Name, secret.value); err != nil { entry.Status = SecretFileFailed return report, secretsFailure(CodeSecretsApplyFailed, "secret environment variable could not be set for file: "+strconv.Quote(entry.Path)) } entry.Status = SecretFileLoaded } return report, nil } func readSecretsEntries(directory *os.File) ([]fs.DirEntry, error) { entries := make([]fs.DirEntry, 0) for len(entries) <= secretsEntryLimit { batch, err := directory.ReadDir(secretsEntryLimit + 1 - len(entries)) entries = append(entries, batch...) if err == io.EOF { return entries, nil } if err != nil { return entries, err } if len(entries) > secretsEntryLimit { return entries, nil } } return entries, nil } type secretReadFailure int const ( secretReadFilesystem secretReadFailure = iota secretReadFileLimit secretReadTotalLimit secretReadNUL ) type secretReadError struct { kind secretReadFailure err error } func (e *secretReadError) Error() string { return "secret read failed" } func readSecretValue(path string, remaining int64, access secretsAccess) (string, int64, error) { if remaining < 0 { return "", 0, &secretReadError{kind: secretReadTotalLimit} } limit := min(secretFileByteLimit, remaining) file, err := access.openFile(path) if err != nil { return "", 0, &secretReadError{kind: secretReadFilesystem, err: err} } data, readErr := io.ReadAll(io.LimitReader(file, limit+1)) closeErr := file.Close() if readErr != nil { return "", 0, &secretReadError{kind: secretReadFilesystem, err: readErr} } if closeErr != nil { return "", 0, &secretReadError{kind: secretReadFilesystem, err: closeErr} } if int64(len(data)) > limit { kind := secretReadFileLimit if remaining < secretFileByteLimit { kind = secretReadTotalLimit } return "", 0, &secretReadError{kind: kind} } if slices.Contains(data, byte(0)) { return "", 0, &secretReadError{kind: secretReadNUL} } value := string(data) if strings.HasSuffix(value, "\r\n") { value = strings.TrimSuffix(value, "\r\n") } else { value = strings.TrimSuffix(value, "\n") } return value, int64(len(data)), nil } func validEnvironmentName(name string) bool { if name == "" || !asciiEnvironmentFirst(name[0]) { return false } for index := 1; index < len(name); index++ { character := name[index] if !asciiEnvironmentFirst(character) && (character < '0' || character > '9') { return false } } return true } func asciiEnvironmentFirst(character byte) bool { return character == '_' || character >= 'A' && character <= 'Z' || character >= 'a' && character <= 'z' } func cleanAbsolutePath(path string) string { absolute, err := filepath.Abs(path) if err != nil { return filepath.Clean(path) } return filepath.Clean(absolute) } func classifySecretsFilesystemError(err error) Code { switch { case errors.Is(err, fs.ErrNotExist): return CodeSecretsMissing case errors.Is(err, fs.ErrPermission): return CodeSecretsPermission default: return CodeSecretsRead } } func secretReadCode(err error) Code { var failure *secretReadError if errors.As(err, &failure) { switch failure.kind { case secretReadFileLimit, secretReadTotalLimit: return CodeSecretsLimitExceeded case secretReadNUL: return CodeSecretsInvalidValue case secretReadFilesystem: return classifySecretsFilesystemError(failure.err) } } return CodeSecretsRead } func secretReadMessage(err error) string { var failure *secretReadError if errors.As(err, &failure) { switch failure.kind { case secretReadFileLimit: return "secret file exceeds the 1 MiB limit" case secretReadTotalLimit: return "eligible secret files exceed the 8 MiB total limit" case secretReadNUL: return "secret file contains a NUL byte" } } return "secret file could not be read" } func secretsFailure(code Code, message string) *SecretsError { return &SecretsError{diagnostics: []Diagnostic{{Code: code, Severity: SeverityError, Message: message}}} }