Emit audit events for instance SSH certificate create and delete

Overview

Emit audit events when an admin adds or removes an instance-level trusted CA. Adding a CA grants instance-wide Git access to anyone holding a certificate it signed, and removing one revokes that access immediately — both are security-relevant configuration changes that need an audit trail.

Mirrors the group-level equivalent, #427413 (closed).

Reference implementation

The group services do this by prepending an EE module that wraps execute:

module EE
  module Groups
    module SshCertificates
      module CreateService
        extend ::Gitlab::Utils::Override

        override :execute
        def execute
          response = super
          log_audit_event(response.payload) if response.success?
          response
        end

        private

        def log_audit_event(ssh_certificate)
          audit_context = {
            name: "create_ssh_certificate",
            author: current_user,
            scope: group,
            target: ssh_certificate,
            target_details: ssh_certificate.title,
            message: "Created SSH certificate with id #{ssh_certificate.id} and title #{ssh_certificate.title}"
          }

          ::Gitlab::Audit::Auditor.audit(audit_context)
        end
      end
    end
  end
end

Files: ee/app/services/ee/groups/ssh_certificates/{create,destroy}_service.rb.

The base services in #611314 (closed) already call prepend_mod_with, so the same wrapping approach applies.

Proposal

1. Event definitions

Two new events:

Name Emitted when
create_instance_ssh_certificate An admin adds a CA
delete_instance_ssh_certificate An admin removes a CA

Distinct from the group-level create_ssh_certificate / delete_ssh_certificate names, so the two scopes are separable in a compliance report.

Each needs a YAML definition under ee/config/audit_events/types/.

2. Audit context

audit_context = {
  name: "create_instance_ssh_certificate",
  author: current_user,
  scope: ::Gitlab::Audit::InstanceScope.new,
  target: ssh_certificate,
  target_details: ssh_certificate.title,
  message: "Created instance SSH certificate with id #{ssh_certificate.id} " \
           "and title #{ssh_certificate.title}"
}

The scope is instance-level rather than a group, so use Gitlab::Audit::InstanceScope. Confirm the exact class during implementation — this is the one meaningful divergence from the group implementation and determines where the event surfaces in the admin audit log.

Include the fingerprint in target_details or the message. The group implementation logs only id and title, but the fingerprint is what identifies the CA in gitlab-sshd logs and in an admin's own CA inventory, so it is what makes the event useful during an incident. Do not log the key itself.

3. Where to emit

Wrap execute in InstanceSshCertificates::CreateService and DestroyService from #611314 (closed), emitting only on success. This covers both the REST API and the Admin UI, since both go through the services.

Licensing note

Audit events are a licensed feature, while this epic targets all tiers. On a Free instance the CAs will work but produce no audit trail.

That is acceptable — the FedRAMP and Dedicated customers driving this requirement are on Ultimate — but it should be a conscious decision rather than a surprise. The audit code lives in ee/, so this falls out naturally from where it is placed.

Acceptance criteria

  • create_instance_ssh_certificate emitted on successful creation.
  • delete_instance_ssh_certificate emitted on successful deletion.
  • No event emitted when the operation fails.
  • Events use an instance-level audit scope and appear in the admin audit log.
  • Event includes author, certificate id, title, and fingerprint.
  • The CA key itself is never written to an audit event.
  • Both events have YAML definitions.
  • Events fire for both API and UI paths.
  • Specs cover success and failure for each service.

Out of scope

  • Auth-time audit events for certificate-based authentication — separate issue, different code path and volume profile.