{"record":{"id":"a504af46f380cfde","repo":"goharbor/harbor","slug":"unauthorized-a504af","errorCode":null,"errorMessage":"unauthorized","messagePattern":"unauthorized","errorType":"http","errorClass":null,"httpStatus":401,"severity":"critical","filePath":"src/jobservice/api/authenticator.go","lineNumber":71,"sourceCode":"\n\th := strings.TrimSpace(req.Header.Get(authHeader))\n\tif utils.IsEmptyStr(h) {\n\t\treturn fmt.Errorf(\"header '%s' missing\", authHeader)\n\t}\n\n\tif !strings.HasPrefix(h, secretPrefix) {\n\t\treturn fmt.Errorf(\"'%s' should start with '%s'\", authHeader, secretPrefix)\n\t}\n\n\tsecret := strings.TrimSpace(strings.TrimPrefix(h, secretPrefix))\n\t// incase both two are empty\n\tif utils.IsEmptyStr(secret) {\n\t\treturn errors.New(\"empty secret is not allowed\")\n\t}\n\n\texpectedSecret := config.GetUIAuthSecret()\n\tif subtle.ConstantTimeCompare([]byte(expectedSecret), []byte(secret)) == 0 {\n\t\treturn errors.New(\"unauthorized\")\n\t}\n\n\treturn nil\n}\n","sourceCodeStart":53,"sourceCodeEnd":76,"githubUrl":"https://github.com/goharbor/harbor/blob/7b2fd08cc568955cca339afeefab27372840d936/src/jobservice/api/authenticator.go#L53-L76","documentation":"Jobservice DoAuth compares the presented secret against config.GetUIAuthSecret() with subtle.ConstantTimeCompare and returns 'unauthorized' on mismatch. The caller's secret - normally core's jobservice secret - does not equal what this jobservice instance was configured with, so service-to-service auth fails.","triggerScenarios":"Any core-to-jobservice API call (submit job, query status, fetch logs) after the shared secret drifted between the two deployments.","commonSituations":"Only one component redeployed so its secret rotated independently; secret edited in one env file but not the other; Helm values out of sync between core and jobservice.","solutions":["Set the identical jobservice secret in core's config and jobservice's config (single shared value/env var)","Restart both core and jobservice after changing the secret","In compose/Helm, source the secret from one shared configuration entry so drift cannot happen"],"exampleFix":"# before: secrets drifted\n# core env:      JOBSERVICE_SECRET=aaa\n# jobservice env: JOBSERVICE_SECRET=bbb\n\n# after: one shared value, both services restarted\nexport JOBSERVICE_SECRET=$(openssl rand -hex 16)  # use for BOTH deployments","handlingStrategy":"validation","validationCode":"// Deployment guard: both sides must derive the secret from the same source\nif coreSecret != jobserviceSecret {\n    return errors.New(\"core and jobservice secrets differ: abort deploy\")\n}","typeGuard":"func isUnauthorizedSecret(err error) bool { return err != nil && strings.Contains(err.Error(), \"unauthorized\") }","tryCatchPattern":"if err := sa.DoAuth(req); err != nil {\n    if strings.Contains(err.Error(), \"unauthorized\") {\n        // secret drift: do NOT retry; re-sync secrets and restart both services\n        return errors.New(\"jobservice secret mismatch: re-align configuration\")\n    }\n    return err\n}","preventionTips":["Source the jobservice secret from one shared config value/env var","Restart core and jobservice together after any secret change","Add a deployment check comparing secrets before rollout"],"tags":["harbor","jobservice","auth","secret","service-to-service"],"backgroundTag":null,"analyzedSha":"7b2fd08cc568955cca339afeefab27372840d936","analyzedAt":"2026-08-16T00:00:10.961Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}