OpenNHP/opennhp · error

keystore: create directory

Error message

keystore: create directory %s: %w

What it means

NewAgentKeyStore creates the parent directory of the SQLite database path with os.MkdirAll(dir, 0700) before opening the DB; if directory creation fails it returns 'keystore: create directory %s: %w'. Typical causes are permission denial on the parent, a non-directory existing at that path, or a read-only filesystem. The wrapped error carries the exact OS reason.

Solutions

  1. Create/fix the data directory manually with correct ownership: `mkdir -p <dir> && chown <serveruser> <dir>`.
  2. Check the wrapped errno: ENOTDIR means a file occupies the path — remove/rename it; EACCES means fix permissions; EROFS means remount read-write.
  3. If running under systemd, add the DB directory to ReadWritePaths= (or set StateDirectory=).
  4. Pass an explicit writable dbPath in configuration instead of relying on the relative default data/nhp_server.db with an unwritable working directory.
  5. In containers, mount a writable volume at the configured path.

Example fix

// before: silent reliance on relative default in a read-only cwd
store, err := NewAgentKeyStore("")
// after: configure an absolute, writable path
store, err := NewAgentKeyStore("/var/lib/nhp-server/nhp_server.db")
// systemd unit:
# StateDirectory=nhp-server  -> /var/lib/nhp-server
Defensive patterns

Strategy: try-catch

Validate before calling

dir := filepath.Dir(dbPath)
if fi, err := os.Stat(dir); err == nil && !fi.IsDir() {
	// a file blocks the directory path; move it before starting
}
if err := syscall.Access(filepath.Dir(dir), syscall.O_RDWR); err != nil {
	// parent not writable by this user
}

Type guard

func writableDir(path string) bool {
	fi, err := os.Stat(path)
	return err == nil && fi.IsDir() && fi.Mode().Perm()&0200 != 0
}

Try / catch

store, err := server.NewAgentKeyStore(dbPath)
if err != nil {
	var perr *os.PathError
	if errors.As(err, &perr) && errors.Is(perr.Err, os.ErrPermission) {
		// fix ownership / run with proper user or StateDirectory
	}
	log.Fatalf("keystore init failed: %v", err)
}

Prevention

When it happens

Trigger: Start (or tests via newTestStore) calls NewAgentKeyStore with a dbPath whose directory cannot be created: EACCES/EPERM (insufficient rights), parent path exists as a regular file (ENOTDIR), read-only filesystem (EROFS), or invalid path characters.

Common situations: Running nhp-serverd as a non-root user whose cwd/data dir is not writable; systemd service with a hardened ReadWritePaths lacking the data dir; config points dbPath at /etc or another root-owned location; a file named 'data' already exists where the directory should be; container volume mounted read-only.

Understand the failure class

Background: mkdir permission denied (EACCES): failed to create directory errors explained — this error's family across 32 libraries.

Related errors


AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07). Data as JSON: /api/errors/9ab918351571007d. Report an issue: GitHub.

Appendix: source

Thrown at endpoints/server/keystore.go:37

type AgentKeyStore struct {
	db *sql.DB
}

// DefaultAgentKeyTTLSeconds is the lifetime of a newly-registered agent
// public key when the operator has not configured agentKeyTTLSeconds.
// 24 hours. Mirrors how OTPTTLSeconds is defaulted at the helper layer.
const DefaultAgentKeyTTLSeconds int64 = 86400

// NewAgentKeyStore opens (or creates) the SQLite database at dbPath.
// The directory is created if it does not exist.
func NewAgentKeyStore(dbPath string) (*AgentKeyStore, error) {
	if dbPath == "" {
		dbPath = filepath.Join("data", "nhp_server.db")
	}

	dir := filepath.Dir(dbPath)
	if err := os.MkdirAll(dir, 0700); err != nil {
		return nil, fmt.Errorf("keystore: create directory %s: %w", dir, err)
	}

	db, err := sql.Open("sqlite", dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
	if err != nil {
		return nil, fmt.Errorf("keystore: open database %s: %w", dbPath, err)
	}

	// Connection pool tuning — SQLite is single-writer; one open conn is
	// usually correct. Keep a small idle pool for concurrent read queries.
	db.SetMaxOpenConns(1)
	db.SetMaxIdleConns(1)
	db.SetConnMaxLifetime(0)

	store := &AgentKeyStore{db: db}
	if err := store.migrate(); err != nil {
		db.Close()
		return nil, fmt.Errorf("keystore: migrate: %w", err)
	}

View on GitHub (pinned to 6e04ca5ff0)