Skip to content

Set SPP as on by default for newly created public projects

Problem to Solve

Currently, newly created public projects do not have Secret Push Protection (SPP) enabled by default, requiring users to manually enable it through security configuration. We need to enable SPP by default for all newly created public projects.

Proposal

As a first step, newly created public projects will have SPP enabled by default on the backend only. Front-end defaults/toggles can follow separately. At the time of this writing (Aug 28th, 2025) Secret push protection is on by default for new... (&18400) will come independently in FY27Q1.

How do users create a 🆕 public project?

There might be a place in the backend workflow where you can set the database value for secret push protection to true when the project is public, but it might be more complex. If the logic is more complex, here are the different ways to create a public project in GitLab:

  1. Create via the UI
  2. Create via the API
  3. Import via UI
  4. Import via API Script
  5. Import from GitHub
  6. Direct Transfer Migration
  7. Professional Services Migration

How will users disable secret push protection?

Maintainers and Owners will continue to be able to toggle on/off secret push protection via the REST API endpoint or the GraphQL mutation) on a per project level. We will also rework the existing Secret Push Protection configuration UI to be toggleable by public projects only: #567659

Implementation Plan

Below is the initial investigation and context regarding project_security_settings. Through further investigation, we have found that project_security_settings are already created for projects across all tiers during project creation, so no model move is required.

Click to expand initial context

Before we jump into the implementation plan, there's some important context to understand below.

EE vs. CE

SPP was built as an Ultimate-only feature, which essentially means it's built to only work when it's licensed to an ultimate project/group, etc. That also means most of the related code resides in EE (under the /ee namespace). In addition to that, with most of the security-related features being ultimate-based too, the associated project_security_settings table which is used to control the enablement status of the feature was also built to solely work with EE projects in mind. That creates some challenges as we plan make the feature work for all public projects regardless of the product tier.

New Projects

As explained in the proposal, new projects gets created in so many different ways across the codebase. Fortunately for us, those all lead to the Projects::CreateService (and EE::Projects::CreateService, if it's an EE-based project) as can be seen below:

  • Create via UI
    • Uses ProjectsController#createProjects::CreateService
  • Create via API
    • Uses API::Projects#postProjects::CreateService
  • Import via UI
    • Uses various import controllersProjects::CreateService
  • Import via API Script
    • Uses API endpointsProjects::CreateService
  • Import from GitHub
    • Uses Import::GithubServiceGitlab::LegacyGithubImport::ProjectCreatorProjects::CreateService
  • Direct Transfer Migration
    • Uses BulkImports::Projects::Pipelines::ProjectPipelineProjects::CreateService
  • Professional Services Migration
    • Uses GitLab project importProjects::GitlabProjectsImportServiceProjects::CreateService

So that gives us one option:

  • To inject the code necessary to create the associated project_security_setting record.
  • To set secret_push_protection_enabled to true when the project is created.

Another option (which I personally prefer) is:

  • To use lifecycle hooks, e.g. after_create, which we do in EE::Project to create the associated project_security_setting if it doesn't exist. In this case, we could update the lifecycle method to check if the project is public, and depending on this, whether to set secret_push_protection_enabled to true or not.

Based on the two topics above, we should likely do the following list of tasks to implement this:

1️⃣ Update lifecycle hook to enable SPP if project is public

  • In the project creation lifecycle hook that builds the project_security_setting, update create_security_setting to set secret_push_protection_enabled to true if the project is public (see example).

Example Code

# In app/models/ee/project.rb

private

def create_security_setting
  return if security_setting # maybe not return early to also confirm we have this enabled for public projects?

  # Default SPP to enabled for public projects, disabled for others
  public_project = public?
  
  build_security_setting(
    secret_push_protection_enabled: public_project
  ).save!
end

2️⃣ Update license checks across the codebase, and EligibilityChecker

  • Update license checks across the codebase to only check for ultimate if it's a private project.
  • Update EligibilityChecker to only check for secret_push_protection_enabled? for public projects (see diagram below).

Example Diagram

flowchart LR
    Start([Eligibility Checker]) --> Decision{Public?}
    
    Decision -->|Yes| CheckFlagPublic{SPP Setting Check}
    CheckFlagPublic -->|Yes| EnablePublic[Project can run SPP scan]
    CheckFlagPublic -->|No| DisablePublic[Project not eligible for SPP scan]
    
    Decision -->|No| CheckLicense{Ultimate?}
    CheckLicense -->|Yes| CheckFlagPrivate{SPP Setting Check}
    CheckLicense -->|No| DisableLicense[Project not eligible for SPP scan]
    
    CheckFlagPrivate -->|Yes| EnablePrivate[Project can run SPP scan]
    CheckFlagPrivate -->|No| DisablePrivate[Project not eligible for SPP scan]

Note: for brevity only certain parts of the eligibility checker are represented here.

Edited by Serena Fang