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

  1. Create a service account in Secrets Manager and use its access token for sync calls.
  2. Switch the client to authenticate with the service-account credential flow.
  3. 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

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


AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13). Data as JSON: /api/errors/e2687dc96d23f06d. Report an issue: GitHub.