{"record":{"id":"ec0a12c17a4050c9","repo":"juanfont/headscale","slug":"generating-salt-w","errorCode":null,"errorMessage":"generating salt: %w","messagePattern":"generating salt: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/db/oauth.go","lineNumber":75,"sourceCode":"\targon2KeyLen  = 32\n\targon2SaltLen = 16\n)\n\n// argon2Limiter bounds concurrent Argon2id computations. Each costs ~19 MiB and\n// the unauthenticated OAuth token endpoint runs one per attempt, so an unbounded\n// flood could exhaust memory. ponytail: a global semaphore sized to GOMAXPROCS;\n// revisit only if credential hashing ever becomes a throughput bottleneck.\nvar argon2Limiter = make(chan struct{}, max(2, runtime.GOMAXPROCS(0)))\n\n// hashSecret hashes a credential secret with Argon2id, encoded in PHC string\n// form so the parameters travel with the hash. Argon2id is the current OWASP\n// recommendation, replacing bcrypt for new credential storage.\nfunc hashSecret(secret string) ([]byte, error) {\n\tsalt := make([]byte, argon2SaltLen)\n\n\t_, err := rand.Read(salt)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"generating salt: %w\", err)\n\t}\n\n\thash := argon2.IDKey([]byte(secret), salt, argon2Time, argon2Memory, argon2Threads, argon2KeyLen)\n\n\tencoded := fmt.Sprintf(\"$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s\",\n\t\targon2.Version, argon2Memory, argon2Time, argon2Threads,\n\t\tbase64.RawStdEncoding.EncodeToString(salt),\n\t\tbase64.RawStdEncoding.EncodeToString(hash),\n\t)\n\n\treturn []byte(encoded), nil\n}\n\n// verifySecret reports whether secret matches a hashSecret-encoded hash. It\n// reads the cost parameters from the stored hash and compares in constant time\n// so a mismatch leaks no timing signal.\nfunc verifySecret(encoded []byte, secret string) error {\n\tparts := strings.Split(string(encoded), \"$\")","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/oauth.go#L57-L93","documentation":"hashSecret generates the Argon2id salt with crypto/rand.Read before deriving the key. This error means the OS entropy source failed — on Linux, getrandom(2) virtually never fails after boot, so seeing this indicates a serious environment problem, not an application bug. No credential is persisted when it fires.","triggerScenarios":"Early-boot entropy exhaustion on minimal VMs/containers; a broken /dev/urandom mount or seccomp/filter blocking getrandom in exotic sandboxes; file-descriptor exhaustion in older Go runtimes.","commonSituations":"Tiny containers with restrictive syscall filters; embedded devices booted straight into the service.","solutions":["Check the host entropy source (getrandom availability, /dev/urandom permissions)","Relax the container seccomp/apparmor profile blocking getrandom(2)","Fail the credential-creation request; do not fall back to a weaker RNG"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if _, client, err := hsdb.CreateOAuthClient(...); err != nil {\n\tif strings.Contains(err.Error(), \"generating salt\") {\n\t\t// host entropy problem; fail closed, alert ops\n\t}\n\treturn err\n}","preventionTips":["Never substitute a predictable RNG on salt failure — fail the request","Verify getrandom(2) is permitted in container seccomp profiles","Alert on this error: it indicates a broken host, not a bug"],"tags":["go","crypto","argon2","entropy","oauth"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}