bitwarden/server · error · BadRequestException
Only service accounts can sync secrets.
Error message
Only service accounts can sync secrets.
What it means
Thrown at SecretsController.cs:325 inside GetSecretsSyncAsync. The IAccessClientQuery resolved the caller's accessClient to something other than AccessClientType.ServiceAccount, so the controller throws BadRequestException("Only service accounts can sync secrets.") -> HTTP 400. The sync endpoint is machine-only: it is designed to be driven by a service account access token, not a human user session.
Source
Thrown at src/Api/SecretsManager/Controllers/SecretsController.cs:325
[HttpGet("/organizations/{organizationId}/secrets/sync")]
public async Task<SecretsSyncResponseModel> GetSecretsSyncAsync([FromRoute] Guid organizationId,
[FromQuery] DateTime? lastSyncedDate = null)
{
if (lastSyncedDate.HasValue && lastSyncedDate.Value > DateTime.UtcNow)
{
throw new BadRequestException("Last synced date must be in the past.");
}
if (!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
var (accessClient, serviceAccountId) = await _accessClientQuery.GetAccessClientAsync(User, organizationId);
if (accessClient != AccessClientType.ServiceAccount)
{
throw new BadRequestException("Only service accounts can sync secrets.");
}
var syncRequest = new SecretsSyncRequest
{
AccessClientType = accessClient,
OrganizationId = organizationId,
ServiceAccountId = serviceAccountId,
LastSyncedDate = lastSyncedDate
};
var syncResult = await _secretsSyncQuery.GetAsync(syncRequest);
if (syncResult.HasChanges)
{
await LogSecretsEventAsync(syncResult.Secrets, EventType.Secret_Retrieved);
}
return new SecretsSyncResponseModel(syncResult.HasChanges, syncResult.Secrets);
}View on GitHub (pinned to e93b962371)
Solutions
- Create a service account in Secrets Manager and use its access token for sync calls.
- Switch the client to authenticate with the service-account credential flow.
- If a user-driven list is needed, use the regular secrets list endpoints instead of /sync.
Example fix
// before: user token on a machine-only endpoint
await userClient.GetAsync($"/organizations/{org}/secrets/sync"); // 400
// after: authenticate as a service account
var saClient = new HttpClient { DefaultRequestHeaders = Authorization = ServiceAccountAuth(saToken) };
await saClient.GetAsync($"/organizations/{org}/secrets/sync"); Defensive patterns
Strategy: validation
Validate before calling
// Only call /sync with a service-account token
if (await GetIdentityClientTypeAsync(token) != AccessClientType.ServiceAccount)
throw new InvalidOperationException("Sync requires a service account");
await saClient.GetAsync($"/organizations/{orgId}/secrets/sync"); Prevention
- Provision a dedicated service account for sync workloads.
- Never reuse a user OAuth token for machine sync.
- Configure the agent to authenticate via the service-account flow.
When it happens
Trigger: A human user (user session/token, IdentityClientType.User) calls GET /organizations/{org}/secrets/sync instead of a service account. The endpoint exists for machine synchronization.
Common situations: Reusing a user OAuth token in a CI sync script instead of a service account token; misconfigured machine agent authenticated as a user; UI/CLI accidentally pointing the sync flow at a user credential.
Related errors
- Last synced date must be in the past.
- Resource not found.
- You cannot import this much data at once, the limit is 1000
- A secret can only be in one project at a time.
- Resource not found.
AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13).
Data as JSON: /api/errors/e2687dc96d23f06d.
Report an issue: GitHub.