diff --git a/db/docs/batched_background_migrations/backfill_ci_secure_file_states_project_id.yml b/db/docs/batched_background_migrations/backfill_ci_secure_file_states_project_id.yml
new file mode 100644
index 0000000000000000000000000000000000000000..71c8c76c7bd0e3ffd8deb9a9d1545b7ec2788777
--- /dev/null
+++ b/db/docs/batched_background_migrations/backfill_ci_secure_file_states_project_id.yml
@@ -0,0 +1,9 @@
+---
+migration_job_name: BackfillCiSecureFileStatesProjectId
+description: Backfills sharding key `ci_secure_file_states.project_id` from `ci_secure_files`.
+feature_category: secrets_management
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/167617
+milestone: '17.5'
+queued_migration_version: 20240930125311
+finalize_after: '2024-10-22'
+finalized_by: # version of the migration that finalized this BBM
diff --git a/db/docs/ci_secure_file_states.yml b/db/docs/ci_secure_file_states.yml
index 39f559491ece686f89d7d38b417592ed899d1238..796b24c4df4514c322d07af7693d15ad9e6700ec 100644
--- a/db/docs/ci_secure_file_states.yml
+++ b/db/docs/ci_secure_file_states.yml
@@ -17,3 +17,4 @@ desired_sharding_key:
         table: ci_secure_files
         sharding_key: project_id
         belongs_to: ci_secure_file
+desired_sharding_key_migration_job_name: BackfillCiSecureFileStatesProjectId
diff --git a/db/migrate/20240930125308_add_project_id_to_ci_secure_file_states.rb b/db/migrate/20240930125308_add_project_id_to_ci_secure_file_states.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1b096de187bb5fecf20e214f3a183ad812451a4e
--- /dev/null
+++ b/db/migrate/20240930125308_add_project_id_to_ci_secure_file_states.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddProjectIdToCiSecureFileStates < Gitlab::Database::Migration[2.2]
+  milestone '17.5'
+
+  def change
+    add_column :ci_secure_file_states, :project_id, :bigint
+  end
+end
diff --git a/db/post_migrate/20240930125309_index_ci_secure_file_states_on_project_id.rb b/db/post_migrate/20240930125309_index_ci_secure_file_states_on_project_id.rb
new file mode 100644
index 0000000000000000000000000000000000000000..09a1fe51fa27102d2064164ab60a967632c21dfa
--- /dev/null
+++ b/db/post_migrate/20240930125309_index_ci_secure_file_states_on_project_id.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class IndexCiSecureFileStatesOnProjectId < Gitlab::Database::Migration[2.2]
+  milestone '17.5'
+  disable_ddl_transaction!
+
+  INDEX_NAME = 'index_ci_secure_file_states_on_project_id'
+
+  def up
+    add_concurrent_index :ci_secure_file_states, :project_id, name: INDEX_NAME
+  end
+
+  def down
+    remove_concurrent_index_by_name :ci_secure_file_states, INDEX_NAME
+  end
+end
diff --git a/db/post_migrate/20240930125310_add_ci_secure_file_states_project_id_trigger.rb b/db/post_migrate/20240930125310_add_ci_secure_file_states_project_id_trigger.rb
new file mode 100644
index 0000000000000000000000000000000000000000..faa0ff03e329c5a5d58cad00846a3c233ffd5ce4
--- /dev/null
+++ b/db/post_migrate/20240930125310_add_ci_secure_file_states_project_id_trigger.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class AddCiSecureFileStatesProjectIdTrigger < Gitlab::Database::Migration[2.2]
+  milestone '17.5'
+
+  def up
+    install_sharding_key_assignment_trigger(
+      table: :ci_secure_file_states,
+      sharding_key: :project_id,
+      parent_table: :ci_secure_files,
+      parent_sharding_key: :project_id,
+      foreign_key: :ci_secure_file_id
+    )
+  end
+
+  def down
+    remove_sharding_key_assignment_trigger(
+      table: :ci_secure_file_states,
+      sharding_key: :project_id,
+      parent_table: :ci_secure_files,
+      parent_sharding_key: :project_id,
+      foreign_key: :ci_secure_file_id
+    )
+  end
+end
diff --git a/db/post_migrate/20240930125311_queue_backfill_ci_secure_file_states_project_id.rb b/db/post_migrate/20240930125311_queue_backfill_ci_secure_file_states_project_id.rb
new file mode 100644
index 0000000000000000000000000000000000000000..44645c60221e3051924355aa9bc26fd42659d942
--- /dev/null
+++ b/db/post_migrate/20240930125311_queue_backfill_ci_secure_file_states_project_id.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+class QueueBackfillCiSecureFileStatesProjectId < Gitlab::Database::Migration[2.2]
+  milestone '17.5'
+  restrict_gitlab_migration gitlab_schema: :gitlab_ci
+
+  MIGRATION = "BackfillCiSecureFileStatesProjectId"
+  DELAY_INTERVAL = 2.minutes
+  BATCH_SIZE = 1000
+  SUB_BATCH_SIZE = 100
+
+  def up
+    queue_batched_background_migration(
+      MIGRATION,
+      :ci_secure_file_states,
+      :ci_secure_file_id,
+      :project_id,
+      :ci_secure_files,
+      :project_id,
+      :ci_secure_file_id,
+      job_interval: DELAY_INTERVAL,
+      batch_size: BATCH_SIZE,
+      sub_batch_size: SUB_BATCH_SIZE
+    )
+  end
+
+  def down
+    delete_batched_background_migration(
+      MIGRATION,
+      :ci_secure_file_states,
+      :ci_secure_file_id,
+      [
+        :project_id,
+        :ci_secure_files,
+        :project_id,
+        :ci_secure_file_id
+      ]
+    )
+  end
+end
diff --git a/db/schema_migrations/20240930125308 b/db/schema_migrations/20240930125308
new file mode 100644
index 0000000000000000000000000000000000000000..3ad785c1bb05e25981507b755c190958e903f08f
--- /dev/null
+++ b/db/schema_migrations/20240930125308
@@ -0,0 +1 @@
+b226424a525abb340c104a6c8d5debc5ad0297220b37bff32aaeaa767702d21a
\ No newline at end of file
diff --git a/db/schema_migrations/20240930125309 b/db/schema_migrations/20240930125309
new file mode 100644
index 0000000000000000000000000000000000000000..248462abefa7453486e9e54fd643040c7b0a2cd5
--- /dev/null
+++ b/db/schema_migrations/20240930125309
@@ -0,0 +1 @@
+fc748d24f09f804e50dc62a9e9f5f5941ecd7235a55760f84bfd1c5dd2a95edf
\ No newline at end of file
diff --git a/db/schema_migrations/20240930125310 b/db/schema_migrations/20240930125310
new file mode 100644
index 0000000000000000000000000000000000000000..abca0c65f00945a6897a9ed9e0e3ce694d87a9b1
--- /dev/null
+++ b/db/schema_migrations/20240930125310
@@ -0,0 +1 @@
+6634a974d38adbe6d05a40ccfbd6e5d7469bc2462e7784c70e52e8ecbdb8a447
\ No newline at end of file
diff --git a/db/schema_migrations/20240930125311 b/db/schema_migrations/20240930125311
new file mode 100644
index 0000000000000000000000000000000000000000..09a5867fa648e59875cc4b9a76de33608c2ca9df
--- /dev/null
+++ b/db/schema_migrations/20240930125311
@@ -0,0 +1 @@
+64a55d736a19fb1a04f3f046808e3f9d2f88485a85a9c44b119e4756ad38187e
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 28f58af15e0cee5145004dfb9fdfd4642c8bc930..c24845b8ffa81b9c6743a1f545e8bd9c24c80faf 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -1801,6 +1801,22 @@ RETURN NEW;
 END
 $$;
 
+CREATE FUNCTION trigger_8b39d532224c() RETURNS trigger
+    LANGUAGE plpgsql
+    AS $$
+BEGIN
+IF NEW."project_id" IS NULL THEN
+  SELECT "project_id"
+  INTO NEW."project_id"
+  FROM "ci_secure_files"
+  WHERE "ci_secure_files"."id" = NEW."ci_secure_file_id";
+END IF;
+
+RETURN NEW;
+
+END
+$$;
+
 CREATE FUNCTION trigger_8ba31bddd655() RETURNS trigger
     LANGUAGE plpgsql
     AS $$
@@ -9095,6 +9111,7 @@ CREATE TABLE ci_secure_file_states (
     verification_retry_count smallint,
     verification_checksum bytea,
     verification_failure text,
+    project_id bigint,
     CONSTRAINT check_a79e5a9261 CHECK ((char_length(verification_failure) <= 255))
 );
 
@@ -28379,6 +28396,8 @@ CREATE INDEX index_ci_secure_file_states_needs_verification ON ci_secure_file_st
 
 CREATE INDEX index_ci_secure_file_states_on_ci_secure_file_id ON ci_secure_file_states USING btree (ci_secure_file_id);
 
+CREATE INDEX index_ci_secure_file_states_on_project_id ON ci_secure_file_states USING btree (project_id);
+
 CREATE INDEX index_ci_secure_file_states_on_verification_state ON ci_secure_file_states USING btree (verification_state);
 
 CREATE INDEX index_ci_secure_file_states_pending_verification ON ci_secure_file_states USING btree (verified_at NULLS FIRST) WHERE (verification_state = 0);
@@ -33689,6 +33708,8 @@ CREATE TRIGGER trigger_8a38ce2327de BEFORE INSERT OR UPDATE ON boards_epic_user_
 
 CREATE TRIGGER trigger_8ac78f164b2d BEFORE INSERT OR UPDATE ON design_management_repositories FOR EACH ROW EXECUTE FUNCTION trigger_8ac78f164b2d();
 
+CREATE TRIGGER trigger_8b39d532224c BEFORE INSERT OR UPDATE ON ci_secure_file_states FOR EACH ROW EXECUTE FUNCTION trigger_8b39d532224c();
+
 CREATE TRIGGER trigger_8ba31bddd655 BEFORE INSERT OR UPDATE ON vulnerability_occurrence_pipelines FOR EACH ROW EXECUTE FUNCTION trigger_8ba31bddd655();
 
 CREATE TRIGGER trigger_8d002f38bdef BEFORE INSERT OR UPDATE ON packages_debian_group_components FOR EACH ROW EXECUTE FUNCTION trigger_8d002f38bdef();
diff --git a/lib/gitlab/background_migration/backfill_ci_secure_file_states_project_id.rb b/lib/gitlab/background_migration/backfill_ci_secure_file_states_project_id.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2d19fdaf963d44b000103bf468fcc4cb896fa84f
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_ci_secure_file_states_project_id.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module BackgroundMigration
+    class BackfillCiSecureFileStatesProjectId < BackfillDesiredShardingKeyJob
+      operation_name :backfill_ci_secure_file_states_project_id
+      feature_category :secrets_management
+    end
+  end
+end
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 3ec0b7ef8ce0fa0a733509b03dd8af6c23354e11..580df2d6e15aebd97b56acba1db380ac8d1313c3 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -95,6 +95,7 @@
     ci_pipeline_schedule_variables: %w[project_id],
     ci_pipeline_variables: %w[partition_id pipeline_id project_id],
     ci_pipelines: %w[partition_id auto_canceled_by_partition_id],
+    ci_secure_file_states: %w[project_id],
     ci_unit_test_failures: %w[project_id],
     ci_resources: %w[project_id],
     p_ci_pipelines: %w[partition_id auto_canceled_by_partition_id auto_canceled_by_id],
diff --git a/spec/lib/gitlab/background_migration/backfill_ci_secure_file_states_project_id_spec.rb b/spec/lib/gitlab/background_migration/backfill_ci_secure_file_states_project_id_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cc28a1e2e1012cb93816e12f7d970f2c2872aa5c
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_ci_secure_file_states_project_id_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillCiSecureFileStatesProjectId,
+  feature_category: :secrets_management,
+  schema: 20240930125308,
+  migration: :gitlab_ci do
+  include_examples 'desired sharding key backfill job' do
+    let(:batch_table) { :ci_secure_file_states }
+    let(:batch_column) { :ci_secure_file_id }
+    let(:backfill_column) { :project_id }
+    let(:backfill_via_table) { :ci_secure_files }
+    let(:backfill_via_column) { :project_id }
+    let(:backfill_via_foreign_key) { :ci_secure_file_id }
+  end
+end
diff --git a/spec/migrations/20240930125312_queue_backfill_ci_secure_file_states_project_id_spec.rb b/spec/migrations/20240930125312_queue_backfill_ci_secure_file_states_project_id_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d0ca7b6d9955c930cb9e56f5c5a0af487cf18b9f
--- /dev/null
+++ b/spec/migrations/20240930125312_queue_backfill_ci_secure_file_states_project_id_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueBackfillCiSecureFileStatesProjectId, migration: :gitlab_ci, feature_category: :secrets_management do
+  let!(:batched_migration) { described_class::MIGRATION }
+
+  it 'schedules a new batched migration' do
+    reversible_migration do |migration|
+      migration.before -> {
+        expect(batched_migration).not_to have_scheduled_batched_migration
+      }
+
+      migration.after -> {
+        expect(batched_migration).to have_scheduled_batched_migration(
+          table_name: :ci_secure_file_states,
+          column_name: :ci_secure_file_id,
+          interval: described_class::DELAY_INTERVAL,
+          batch_size: described_class::BATCH_SIZE,
+          sub_batch_size: described_class::SUB_BATCH_SIZE,
+          gitlab_schema: :gitlab_ci,
+          job_arguments: [
+            :project_id,
+            :ci_secure_files,
+            :project_id,
+            :ci_secure_file_id
+          ]
+        )
+      }
+    end
+  end
+end