OpenNHP/opennhp · error
failed to create etc directory
Error message
failed to create etc directory: %v
What it means
DataPrivateKeyStore.Save persists the data private key under etc/ztdo and first ensures that directory exists via os.MkdirAll(etcDir, 0755). If directory creation fails - permission denied, path exists as a regular file, or read-only filesystem - Save returns this error and no key file is written.
Solutions
- Check the wrapped os error: for 'permission denied', grant the process user write access to <exeDir> (chown/chmod) or run with an account that has it.
- Ensure etc/ztdo's parent is a writable directory, not a read-only mount; remount rw or point the daemon at a writable location.
- Verify 'etc' under the executable directory is a directory (mv a stray 'etc' file out of the way).
- In Docker, mount a volume for etc/ztdo (e.g. -v nhp-db-etc:/app/etc/ztdo) instead of writing into the image layer.
Example fix
// before: docker run with read-only rootfs and no volume // docker run --read-only opennhp/nhp-db // after: writable volume for the key store // docker run --read-only -v nhp-db-etc:/app/etc/ztdo opennhp/nhp-db
Defensive patterns
Strategy: validation
Validate before calling
dir := filepath.Join(common.ExeDirPath, "etc")
if st, err := os.Stat(dir); err == nil && !st.IsDir() {
return fmt.Errorf("%s exists and is not a directory", dir)
}
if err := syscall.Access(dir, syscall.W_OK); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("no write permission on %s: %w", dir, err)
} Type guard
func canWriteDir(path string) bool {
st, err := os.Stat(path)
return err == nil && st.IsDir() && unix.Access(path, unix.W_OK) == nil
} Try / catch
if err := store.Save(doId); err != nil {
if errors.Is(errors.Unwrap(err), fs.ErrPermission) {
log.Fatalf("cannot write key dir: fix permissions on %s: %v", common.ExeDirPath, err)
}
return err
} Prevention
- Provision etc/ztdo with correct ownership at install time instead of relying on MkdirAll at runtime.
- Do not run daemons from read-only directories; keep writable state on a dedicated volume.
- In systemd units, add ReadWritePaths=<exeDir>/etc for hardening options like ProtectSystem.
- Check that no stray file named 'etc' exists under the executable directory.
When it happens
Trigger: Calling Save(doId) when: (1) etc/ztdo cannot be created because the process user lacks write permission on <exeDir>; (2) 'etc' already exists as a regular file rather than a directory; (3) the filesystem containing common.ExeDirPath is mounted read-only (e.g. container image layer, read-only volume).
Common situations: Running nhp-db as non-root in a container where the exe directory is read-only; systemd service with ProtectSystem=strict without ReadWritePaths for etc/; a stray file named etc blocking MkdirAll.
Understand the failure class
Background: mkdir permission denied (EACCES): failed to create directory errors explained — this error's family across 32 libraries.
Related errors
- failed to create etc directory
- failed to create file
- keystore: create directory
- failed to open file
- resource not found
AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07).
Data as JSON: /api/errors/1c97005f34dcd925.
Report an issue: GitHub.
Appendix: source
Thrown at endpoints/db/utils.go:68
d = &DataPrivateKeyStore{}
_ = d.fromJson(fileContentByte)
return
}
func (d *DataPrivateKeyStore) Generate(mode ztdolib.DataKeyPairECCMode) (privateKey []byte) {
ecdh := core.NewECDH(mode.ToEccType())
d.DataPrivateKeyBase64 = ecdh.PrivateKeyBase64()
return ecdh.PrivateKey()
}
// Save saves the dataPrivateKeyBase64 to a file, the format of file name is data-<doId>.json
// Notes: this default way to store data private key is not safe. In the wild environment, need to use a secure way to store data private key.
func (d *DataPrivateKeyStore) Save(doId string) error {
// Make sure the etc directory exists
etcDir := "etc/ztdo"
if err := os.MkdirAll(etcDir, 0755); err != nil {
return fmt.Errorf("failed to create etc directory: %v", err)
}
fileName := "data-key-" + doId + ".json"
fullPath := filepath.Join(common.ExeDirPath, etcDir, fileName)
if _, err := os.Stat(fullPath); err == nil {
return fmt.Errorf("%v already exists, please delete it first", fullPath)
}
file, err := os.Create(fullPath)
if err != nil {
return fmt.Errorf("failed to create file: %v", err)
}
defer file.Close()
_, err = file.Write(d.toJson())
return err
}
View on GitHub (pinned to 6e04ca5ff0)