Add audit event for service account creation
Summary
When a service account is created (via API or UI at the instance, group, or project level), no dedicated audit event is recorded. This is a gap in audit coverage, as service accounts have elevated access and their creation should be tracked for compliance and security purposes.
There is a generic user_created audit event that fires for regular user creation (via the EE override in ee/app/services/ee/users/create_service.rb), but it provides no service-account-specific context. A dedicated event is needed for security monitoring and SIEM integration.
A related issue exists for DAP (Duo Agent Platform) auto-provisioned service accounts (#593021), but this issue covers all service account creation paths.
Proposal
Add a new service_account_created audit event that fires whenever a service account is successfully created, regardless of the entry point.
Class hierarchy and entry points
The service account creation class hierarchy is:
Users::ServiceAccounts::CreateService < BaseService
├── Namespaces::ServiceAccounts::CreateService (used for group/project-level creation)
└── Namespaces::ServiceAccounts::BaseCreateService (sibling class, appears unused in production code)Important: BaseCreateService and CreateService are siblings, not parent-child. Both inherit independently from Users::ServiceAccounts::CreateService. BaseCreateService has no references in production code (only its own spec references it) and may be dead code or reserved for future use. It is not in the current call chain for service account creation.
The active creation paths are:
Users::ServiceAccounts::CreateService(ee/app/services/users/service_accounts/create_service.rb) — Instance-level service account creation. Delegates to::Users::CreateService.Namespaces::ServiceAccounts::CreateService(ee/app/services/namespaces/service_accounts/create_service.rb) — Group/project-level service account creation. Overridescreate_userto delegate to::Users::AuthorizedCreateService.
Since both share the execute method from Users::ServiceAccounts::CreateService, adding the audit call there covers all active paths.
Duplicate audit events — intentional and expected
The EE override at ee/app/services/ee/users/create_service.rb already fires a user_created audit event in after_create_hook whenever current_user is present. Since Users::ServiceAccounts::CreateService#create_user delegates to ::Users::CreateService, every successful service account creation will produce both:
user_created(from the innerUsers::CreateServicevia the EEprependchain)service_account_created(from the new code)
This duplication is intentional and acceptable. The two events serve different purposes:
user_createdis a generic user lifecycle eventservice_account_createdis a security-specific event that enables SIEM rules to distinguish service account creation from regular user creation, and includes the appropriate scope (group/project) rather than just the user
This should be explicitly documented in the MR description so reviewers don't flag it as a bug.
Implementation steps
1. Create the YAML audit event type definition
Create a new file at ee/config/audit_events/types/service_account_created.yml:
---
name: service_account_created
description: A service account is created
introduced_by_issue: <this issue URL>
introduced_by_mr: <MR URL>
feature_category: system_access
milestone: '<current milestone>'
saved_to_database: true
streamed: true
scope: [User, Group, Project]Note on scope values: The valid values per config/audit_events/types/type_schema.json are Instance, Group, Project, and User. The existing user_created.yml uses scope: [User] (not Instance) for instance-level user creation. For consistency, use User for the instance-level scope here as well, alongside Group and Project for namespace-scoped creation.
You can also use the CLI helper to generate the file:
bin/audit-event-type service_account_createdThe YAML schema is defined in config/audit_events/types/type_schema.json. All required fields are: name, description, introduced_by_issue, introduced_by_mr, feature_category, milestone, saved_to_database, streamed, scope.
2. Add the Gitlab::Audit::Auditor.audit call
Updating the execute method
The current execute method in Users::ServiceAccounts::CreateService returns the result of create_user directly:
# Current implementation
def execute
return error(error_messages[:no_permission], :forbidden) unless can_create_service_account?
return error(error_messages[:no_seats], :forbidden) unless creation_allowed?
create_user
endcreate_user delegates to ::Users::CreateService#execute, which returns a ServiceResponse with status (:success or :error) and payload (containing { user: <User> }). The method must be updated to capture the result, audit on success, and return:
# Updated implementation
def execute
return error(error_messages[:no_permission], :forbidden) unless can_create_service_account?
return error(error_messages[:no_seats], :forbidden) unless creation_allowed?
result = create_user
log_service_account_audit_event(result.payload[:user]) if result.success?
result
endThe audit method
# In Users::ServiceAccounts::CreateService (base class)
def audit_scope(user)
user # instance-level, same pattern as user_created
end
def log_service_account_audit_event(user)
::Gitlab::Audit::Auditor.audit({
name: "service_account_created",
author: current_user || ::Gitlab::Audit::UnauthenticatedAuthor.new(name: '(System)'),
scope: audit_scope(user),
target: user,
target_details: user.username,
message: "Created service account #{user.username}"
})
endScope override in the subclass
Namespaces::ServiceAccounts::CreateService already has extend ::Gitlab::Utils::Override, so the override can be added directly:
# In Namespaces::ServiceAccounts::CreateService
# (extend ::Gitlab::Utils::Override is already present in this class)
override :audit_scope
def audit_scope(_user)
resource # the Group or Project
endNote: Users::ServiceAccounts::CreateService (the base class) does not currently have extend ::Gitlab::Utils::Override. If you want to use override annotations in the base class for any reason, you would need to add it. However, since audit_scope is defined (not overridden) in the base class, no override declaration is needed there.
Author handling for system-initiated flows
The implementation uses defensive author handling:
author: current_user || ::Gitlab::Audit::UnauthenticatedAuthor.new(name: '(System)')Even though current manual creation paths always have a current_user, the audit code lives in the base class which could be called from any path in the future (including DAP auto-provisioning covered by #593021).
3. Update documentation
After adding the audit event type, regenerate the audit event types documentation:
bundle exec rake gitlab:audit_event_types:compile_docsVerify it is up-to-date:
bundle exec rake gitlab:audit_event_types:check_docs4. Write tests
Follow the existing test pattern from ee/spec/services/ee/users/create_service_spec.rb:
context 'audit events' do
it 'logs the audit event info' do
expect(::Gitlab::Audit::Auditor).to receive(:audit).with(hash_including(
name: 'service_account_created'
))
service.execute
end
endRequired test cases
- Instance-level creation (
Users::ServiceAccounts::CreateService) — verify event fires withUserscope - Group-level creation (
Namespaces::ServiceAccounts::CreateServicewithnamespace_id) — verify event fires withGroupscope - Project-level creation (
Namespaces::ServiceAccounts::CreateServicewithproject_id) — verify event fires withProjectscope - Failure scenarios — verify the audit event is not created when creation fails (permission denied, no seats)
- Scope correctness — verify the scope is the correct object for each level (not just that the event fires)
target_details— verify it contains the expected username- Dual event verification — verify that both
user_createdandservice_account_createdfire on successful creation (documenting the intentional duplication) skip_owner_check: truepath — verify audit still fires when using the composite identity flowAuditEventrecord verification — in addition to mockingAuditor.audit, verify the actualAuditEventrecord attributes (asee/spec/services/ee/users/create_service_spec.rbdoes withAuditEvent.last)
Developer guidelines reference
The full audit event development guide is at doc/development/audit_event_guide/_index.md. Key points:
- Use
Gitlab::Audit::Auditor.audit(the standard method call for single events) - Do not translate audit event messages (they are stored in the database and served regardless of locale)
- Consider data volume: service account creation is a low-frequency operation, so
saved_to_database: trueis appropriate - All events where the entity is a Group or Project are automatically streamed to configured event streaming destinations
Existing similar audit events for reference
| Event name | File | Scope | Description |
|---|---|---|---|
user_created |
ee/config/audit_events/types/user_created.yml |
[User] |
A user is created |
group_access_token_created |
ee/config/audit_events/types/group_access_token_created.yml |
[Group] |
A group access token is created |
project_access_token_created |
ee/config/audit_events/types/project_access_token_created.yml |
[Project] |
A project access token is created |
Effort estimate
Low — This is a small, well-patterned change. The main work is writing the specs.
Related issues
- #593021 - G6 - Add audit logging for Service Account auto-provisioning (DAP-specific, covers auto-provisioned service accounts only)