diff --git a/lib/entitlements/backend/github_org.rb b/lib/entitlements/backend/github_org.rb
index dc73152..1787cd9 100644
--- a/lib/entitlements/backend/github_org.rb
+++ b/lib/entitlements/backend/github_org.rb
@@ -12,7 +12,8 @@ class GitHubOrg
       ORGANIZATION_ROLES = {
         "admin"  => "ADMIN",
         # `billing-manager` is currently not supported
-        "member" => "MEMBER"
+        "member" => "MEMBER",
+        "security_manager" => "SECURITY-MANAGER"
       }
 
       # Error classes
diff --git a/lib/entitlements/backend/github_org/provider.rb b/lib/entitlements/backend/github_org/provider.rb
index 7f5de3b..f07db1f 100644
--- a/lib/entitlements/backend/github_org/provider.rb
+++ b/lib/entitlements/backend/github_org/provider.rb
@@ -104,7 +104,15 @@ def role_name(role_identifier)
         # Returns an Entitlements::Models::Group object.
         Contract String => Entitlements::Models::Group
         def role_to_group(role)
-          members = github.org_members.keys.select { |username| github.org_members[username] == role }
+          # The security_manager role is a special case because it is not a role that is
+          # part of the org membership API. Instead, it is a role that is assigned to users via
+          # the org role API.
+          if role == "security_manager"
+            members = github.users_with_role(role)
+          else
+            members = github.org_members.keys.select { |username| github.org_members[username] == role }
+          end
+
           Entitlements::Models::Group.new(
             dn: role_dn(role),
             members: Set.new(members),
diff --git a/lib/entitlements/backend/github_org/service.rb b/lib/entitlements/backend/github_org/service.rb
index 0e7e760..8ad77ae 100644
--- a/lib/entitlements/backend/github_org/service.rb
+++ b/lib/entitlements/backend/github_org/service.rb
@@ -35,6 +35,23 @@ def sync(implementation, role)
 
         private
 
+        Contract String, String => C::HashOf[Symbol, C::Any]
+        def add_user_to_role(user, role)
+          if role == "security_manager"
+            octokit.add_role_to_user(user, role)
+
+            # This is a hack to get around the fact that the GitHub API
+            # has two different concepts of organization roles,
+            # and the one we want to use is not present in organization memberships.
+            #
+            # If we get here, we know that the user is already member of the organization,
+            # and we know that the user has been successfully granted the role.
+            { user:, role:, state: "active" }
+          else
+            octokit.update_organization_membership(org, user:, role:)
+          end
+        end
+
         # Upsert a user with a role to the organization.
         #
         # user: A String with the (GitHub) username of the person to add or modify.
@@ -46,10 +63,21 @@ def add_user_to_organization(user, role)
           Entitlements.logger.debug "#{identifier} add_user_to_organization(user=#{user}, org=#{org}, role=#{role})"
 
           begin
-            new_membership = octokit.update_organization_membership(org, user:, role:)
+            new_membership = add_role_to_user(user, role)
           rescue Octokit::NotFound => e
             raise e unless ignore_not_found
 
+            Entitlements.logger.warn "User #{user} not found in GitHub instance #{identifier}, ignoring."
+            return false
+          rescue Octokit::UnprocessableEntity => e
+            # Two conditions can cause this:
+            # - If the role is not enabled, we'll get a 422.
+            # - If the user is not a member of the organization, we'll get a 422.
+
+            # We'll loop this under ignore_not_found
+            # since this affects the case where we want to add a user to security_manager role
+            raise e unless ignore_not_found
+
             Entitlements.logger.warn "User #{user} not found in GitHub instance #{identifier}, ignoring."
             return false
           end
diff --git a/lib/entitlements/service/github.rb b/lib/entitlements/service/github.rb
index 1bfd5a1..c9e52b8 100644
--- a/lib/entitlements/service/github.rb
+++ b/lib/entitlements/service/github.rb
@@ -394,6 +394,32 @@ def max_graphql_results
         MAX_GRAPHQL_RESULTS
       end
       # :nocov:
+
+      Contract C::None => C::ArrayOf[Hash]
+      def org_roles
+        Entitlements.cache[:github_org_roles][org_signature] ||= begin
+          octokit.get("/orgs/#{@org}/organization-roles")
+        end
+      end
+
+      Contract String => C::Maybe[Hash]
+      def org_role(role)
+        org_roles.find { |r| r[:name] == role }
+      end
+
+      Contract String, String => C::Any
+      def add_role_to_user(user, role)
+        role_id = org_role(role)[:id]
+        octokit.put("/orgs/#{org_name}/organization-roles/users/#{user}/#{role_id}")
+      end
+
+      Contract String => C::ArrayOf[Hash]
+      def users_with_role(role)
+        role_id = org_role(role)[:id]
+        Entitlements.cache[:github_org_role_users][org_signature][role] ||= begin
+          octokit.get("/orgs/#{org_name}/organization-roles/#{role_id}/users")
+        end
+      end
     end
   end
 end