diff --git a/doc/guix.texi b/doc/guix.texi
index 981ffb8c58..16fd415b32 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -41683,6 +41683,186 @@ Miscellaneous Services
 @end deftp
 
 
+@c %end of fragment
+
+@cindex Subids
+@subsubheading Subid Service
+
+Among the virtualization facilities implemented by the Linux kernel, the is the
+concept of subordinate IDs.  Subordinate IDs allow for mapping user and group
+IDs inside process namespaces to user and group IDs of the host system.
+Subordinate user ID ranges (subids) allow to map virtual user IDs inside
+containers to the user ID of an unprivileged user of the host system.
+Subordinate group ID ranges (subgids), instead map virtual group IDs to the
+group ID of an unprivileged user on the host system.  You can access
+@code{subuid(5)} and @code{subgid(5)} Linux man pages for more details.
+
+The @code{(gnu system shadow)} module exposes the
+@code{subids-service-type}, its configuration record
+@code{subids-configuration} and its extension record
+@code{subids-extension}.
+
+With @code{subids-service-type}, subuids and subgids ranges can be reserved for
+users that desire so:
+
+@lisp
+(use-modules (gnu system shadow)     ;for 'subids-service-type'
+             (gnu system accounts)   ;for 'subid-range'
+             @dots{})
+
+(operating-system
+  ;; @dots{}
+  (services
+    (list
+      (simple-service 'alice-bob-subids
+                      subids-service-type
+                      (subids-extension
+                        (subgids
+                         (list
+                          (subid-range (name "alice"))))
+                        (subuids
+                         (list
+                          (subid-range (name "alice"))
+                          (subid-range (name "bob")
+                                       (start 100700)))))))))
+@end lisp
+
+Users (definitely other services), usually, are supposed to extend the service
+instead of adding subids directly to @code{subids-configuration}, unless the
+want to change the default behavior for root.  With default settings the
+@code{subids-service-type} adds, if it's not already there, a configuration
+for the root account to both @file{/etc/subuid} and @file{/etc/subgid}, possibly
+starting at the minimum possible subid.  Otherwise the root subuids and subgids
+ranges are fitted wherever possible.
+
+The above configuration will yield the following:
+
+@example
+# cat /etc/subgid
+root:100000:65536
+alice:165536:65536
+# cat /etc/subuid
+root:100000:700
+bob:100700:65536
+alice:166236:65536
+@end example
+
+@c %start of fragment
+
+@deftp {Data Type} subids-configuration
+
+With default settings the
+@code{subids-service-type} adds, if it's not already there, a configuration
+for the root account to both @file{/etc/subuid} and @file{/etc/subgid}, possibly
+starting at the minimum possible subid.  To disable the default behavior and
+provide your own definition for the root subid ranges you can set to @code{#f}
+the @code{add-root?} field:
+
+@lisp
+(use-modules (gnu system shadow)     ;for 'subids-service-type'
+             (gnu system accounts)   ;for 'subid-range'
+             @dots{})
+
+(operating-system
+  ;; @dots{}
+  (services
+    (list
+      (service subids-service-type
+               (subids-configuration
+                (add-root? #f)
+                (subgids
+                  (subid-range (name "root")
+                               (start 120000)
+                               (count 100)))
+                (subuids
+                  (subid-range (name "root")
+                               (start 120000)
+                               (count 100)))))
+      (simple-service 'alice-bob-subids
+                      subids-service-type
+                      (subids-extension
+                        (subgids
+                         (list
+                          (subid-range (name "alice"))))
+                        (subuids
+                         (list
+                          (subid-range (name "alice"))
+                          (subid-range (name "bob")
+                                       (start 100700)))))))))
+@end lisp
+
+Available @code{subids-configuration} fields are:
+
+@table @asis
+@item @code{add-root?} (default: @code{#t}) (type: boolean)
+Whether to automatically configure subuids and subgids for root.
+
+@item @code{subgids} (default: @code{'()}) (type: list-of-subid-ranges)
+The list of @code{subid-range}s that will be serialized to @code{/etc/subgid}.
+If a range doesn't specify a start it will be fitted based on its number of
+requrested subids.  If a range doesn't specify a count the default size
+of 65536 will be assumed.
+
+@item @code{subuids} (default: @code{'()}) (type: list-of-subid-ranges)
+The list of @code{subid-range}s that will be serialized to @code{/etc/subuid}.
+If a range doesn't specify a start it will be fitted based on its number of
+requrested subids.  If a range doesn't specify a count the default size
+of 65536 will be assumed.
+
+@end table
+
+@end deftp
+
+@c %end of fragment
+
+@c %start of fragment
+
+@deftp {Data Type} subids-extension
+
+Available @code{subids-extension} fields are:
+
+@table @asis
+
+@item @code{subgids} (default: @code{'()}) (type: list-of-subid-ranges)
+The list of @code{subid-range}s that will be appended to
+@code{subids-configuration-subgids}.  Entries with the same name are deduplicated
+upon merging.
+
+@item @code{subuids} (default: @code{'()}) (type: list-of-subid-ranges)
+The list of @code{subid-range}s that will be appended to
+@code{subids-configuration-subuids}.  Entries with the same name are deduplicated
+upon merging.
+
+@end table
+
+@end deftp
+
+@c %end of fragment
+
+@c %start of fragment
+
+@deftp {Data Type} subid-range
+
+The @code{subid-range} record is defined at @code{(gnu system accounts)}.
+Available fields are:
+
+@table @asis
+
+@item @code{name} (type: string)
+The name of the user or group that will own this range.
+
+@item @code{start} (default: @code{#f}) (type: integer)
+The first requested subid.  When false the first available subid with enough
+contiguous subids will be assigned.
+
+@item @code{count} (default: @code{#f}) (type: integer)
+The number of total allocated subids.  When #f the default of 65536 will be
+assumed .
+
+@end table
+
+@end deftp
+
 @c %end of fragment
 
 @node Privileged Programs
diff --git a/gnu/build/activation.scm b/gnu/build/activation.scm
index d1a2876a96..5236fbb403 100644
--- a/gnu/build/activation.scm
+++ b/gnu/build/activation.scm
@@ -10,6 +10,7 @@
 ;;; Copyright © 2021 Brice Waegeneire <brice@waegenei.re>
 ;;; Copyright © 2022 Tobias Geerinckx-Rice <me@tobias.gr>
 ;;; Copyright © 2024 Nicolas Graves <ngraves@ngraves.fr>
+;;; Copyright © 2024 Giacomo Leidi <goodoldpaul@autistici.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -40,6 +41,7 @@ (define-module (gnu build activation)
   #:use-module (srfi srfi-11)
   #:use-module (srfi srfi-26)
   #:export (activate-users+groups
+            activate-subuids+subgids
             activate-user-home
             activate-etc
             activate-privileged-programs
@@ -227,6 +229,23 @@ (define (activate-users+groups users groups)
               (chmod directory #o555))
             (duplicates (map user-account-home-directory system-accounts))))
 
+(define (activate-subuids+subgids subuids subgids)
+  "Make sure SUBUIDS (a list of subid range records) and SUBGIDS (a list of
+subid range records) are all available."
+
+  ;; Take same lock as Shadow while we read
+  ;; and write the databases.  This ensures there's no race condition with
+  ;; other tools that might be accessing it at the same time.
+  (with-file-lock "/etc/subgid.lock"
+    (let-values (((subuid subgid)
+                  (subuid+subgid-databases subuids subgids)))
+      (write-subgid subgid)))
+
+  (with-file-lock "/etc/subuid.lock"
+    (let-values (((subuid subgid)
+                  (subuid+subgid-databases subuids subgids)))
+      (write-subuid subuid))))
+
 (define (activate-user-home users)
   "Create and populate the home directory of USERS, a list of tuples, unless
 they already exist."
diff --git a/gnu/local.mk b/gnu/local.mk
index ed630041ff..b36873f28a 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -841,6 +841,7 @@ GNU_SYSTEM_MODULES =				\
   %D%/tests/samba.scm				\
   %D%/tests/security.scm			\
   %D%/tests/security-token.scm			\
+  %D%/tests/shadow.scm  			\
   %D%/tests/singularity.scm			\
   %D%/tests/ssh.scm				\
   %D%/tests/telephony.scm		        \
diff --git a/gnu/system/accounts.scm b/gnu/system/accounts.scm
index 1b88ca301f..f63d7f96bd 100644
--- a/gnu/system/accounts.scm
+++ b/gnu/system/accounts.scm
@@ -51,6 +51,7 @@ (define-module (gnu system accounts)
 
             sexp->user-account
             sexp->user-group
+            sexp->subid-range
 
             default-shell))
 
@@ -159,3 +160,12 @@ (define (sexp->user-account sexp)
                    (create-home-directory? create-home-directory?)
                    (shell shell) (password password)
                    (system? system?)))))
+
+(define (sexp->subid-range sexp)
+  "Take SEXP, a tuple as returned by 'subid-range->gexp', and turn it into a
+subid-range record."
+  (match sexp
+    ((name start count)
+     (subid-range (name name)
+                  (start start)
+                  (count count)))))
diff --git a/gnu/system/shadow.scm b/gnu/system/shadow.scm
index d9f13271d8..48eca2564f 100644
--- a/gnu/system/shadow.scm
+++ b/gnu/system/shadow.scm
@@ -4,6 +4,7 @@
 ;;; Copyright © 2020 Jan (janneke) Nieuwenhuizen <janneke@gnu.org>
 ;;; Copyright © 2020, 2023 Efraim Flashner <efraim@flashner.co.il>
 ;;; Copyright © 2020 Maxim Cournoyer <maxim.cournoyer@gmail.com>
+;;; Copyright © 2024 Giacomo Leidi <goodoldpaul@autistici.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -28,6 +29,10 @@ (define-module (gnu system shadow)
   #:use-module (guix modules)
   #:use-module (guix sets)
   #:use-module (guix ui)
+  #:use-module ((gnu build accounts)
+                #:select (%subordinate-id-count
+                          %subordinate-id-max
+                          %subordinate-id-min))
   #:use-module (gnu system accounts)
   #:use-module (gnu services)
   #:use-module (gnu services shepherd)
@@ -77,7 +82,20 @@ (define-module (gnu system shadow)
             %base-user-accounts
 
             account-service-type
-            account-service))
+            account-service
+
+            subids-configuration
+            subids-configuration?
+            subids-configuration-add-root?
+            subids-configuration-subgids
+            subids-configuration-subuids
+
+            subids-extension
+            subids-extension?
+            subids-extension-subgids
+            subids-extension-subuids
+
+            subids-service-type))
 
 ;;; Commentary:
 ;;;
@@ -380,7 +398,7 @@ (define (assert-valid-users/groups users groups)
 
 
 ;;;
-;;; Service.
+;;; Accounts Service.
 ;;;
 
 (define (user-group->gexp group)
@@ -521,4 +539,193 @@ (define (account-service accounts+groups skeletons)
   (service account-service-type
            (append skeletons accounts+groups)))
 
+
+;;;
+;;; Subids Service.
+;;;
+
+(define* (%root-subid #:optional (start %subordinate-id-min) (count %subordinate-id-count))
+  (subid-range
+   (name "root")
+   (start start)
+   (count count)))
+
+(define-record-type* <subids-configuration>
+  subids-configuration make-subids-configuration
+  subids-configuration?
+  this-subids-configuration
+
+  (add-root?   subids-configuration-add-root?                 ; boolean
+               (default #t))
+  (subgids     subids-configuration-subgids                   ; list of <subid-range>
+               (default '()))
+  (subuids     subids-configuration-subuids                   ; list of <subid-range>
+               (default '())))
+
+(define (subid-range->gexp range)
+  "Turn RANGE, a <subid-range> object, into a list-valued gexp suitable for
+'activate-subuids+subgids'."
+  (define count (subid-range-count range))
+  #~`(#$(subid-range-name range)
+      #$(subid-range-start range)
+      #$(if (and (number? count)
+                 (> count 0))
+            count
+            %subordinate-id-count)))
+
+(define (assert-valid-subids ranges)
+  (cond ((>= (fold + 0 (map subid-range-count ranges))
+             (- %subordinate-id-max %subordinate-id-min -1))
+         (raise
+          (formatted-message
+           (G_
+            "The configured ranges are more than the ~a max allowed.")
+           (- %subordinate-id-max %subordinate-id-min -1))))
+        ((any (lambda (r)
+                (define start (subid-range-start r))
+                (and start
+                     (< start %subordinate-id-min)))
+              ranges)
+         (raise
+          (formatted-message
+           (G_
+            "One subid-range starts before the minimum allowed sub id ~a.")
+           %subordinate-id-min)))
+        ((any (lambda (r)
+                (define end (subid-range-end r))
+                (and end
+                     (> end %subordinate-id-max)))
+              ranges)
+         (raise
+          (formatted-message
+           (G_
+            "One subid-range ends after the maximum allowed sub id ~a.")
+           %subordinate-id-max)))
+        ((any (compose null? subid-range-name)
+              ranges)
+         (raise
+          (formatted-message
+           (G_
+            "One subid-range has a null name."))))
+        ((any (compose string-null? subid-range-name)
+              ranges)
+         (raise
+          (formatted-message
+           (G_
+            "One subid-range has a name equal to the empty string."))))
+        (else #t)))
+
+(define (delete-duplicate-ranges ranges)
+  (delete-duplicates ranges
+                     (lambda args
+                       (apply string=? (map subid-range-name ranges)))))
+
+(define (subids-activation config)
+  "Return a gexp that activates SUBUIDS+SUBGIDS, a list of <subid-range>
+objects."
+  (define (add-root-when-missing ranges)
+    (define sorted-ranges
+      (sort-list ranges subid-range-less))
+    (define root-missing?
+      (not
+       (find (lambda (r)
+               (string=? "root"
+                         (subid-range-name r)))
+             sorted-ranges)))
+    (define first-start
+      (and (> (length sorted-ranges) 0)
+           (subid-range-start (first sorted-ranges))))
+    (define first-has-start?
+      (number? first-start))
+    (define root-start
+      (if first-has-start?
+          (and
+           (> first-start %subordinate-id-min)
+           %subordinate-id-min)
+          %subordinate-id-min))
+    (define root-count
+      (if first-has-start?
+          (- first-start %subordinate-id-min)
+          %subordinate-id-count))
+    (if (and root-missing?
+             (subids-configuration-add-root? config))
+        (append (list (%root-subid root-start root-count))
+                sorted-ranges)
+        sorted-ranges))
+
+  (define subuids
+    (delete-duplicate-ranges (subids-configuration-subuids config)))
+
+  (define subuids-specs
+    (map subid-range->gexp (add-root-when-missing subuids)))
+
+  (define subgids
+    (delete-duplicate-ranges (subids-configuration-subgids config)))
+
+  (define subgids-specs
+    (map subid-range->gexp (add-root-when-missing subgids)))
+
+  (assert-valid-subids subgids)
+  (assert-valid-subids subuids)
+
+  ;; Add subuids and subgids.
+  (with-imported-modules (source-module-closure '((gnu system accounts)))
+    #~(begin
+        (use-modules (gnu system accounts))
+
+        (activate-subuids+subgids (map sexp->subid-range (list #$@subuids-specs))
+                                  (map sexp->subid-range (list #$@subgids-specs))))))
+
+(define-record-type* <subids-extension>
+  subids-extension make-subids-extension
+  subids-extension?
+  this-subids-extension
+
+  (subgids   subids-extension-subgids                   ; list of <subid-range>
+             (default '()))
+  (subuids   subids-extension-subuids                   ; list of <subid-range>
+             (default '())))
+
+(define append-subid-ranges
+  (lambda args
+    (delete-duplicate-ranges
+     (apply append args))))
+
+(define (subids-extension-merge a b)
+  (subids-extension
+   (subgids (append-subid-ranges
+             (subids-extension-subgids a)
+             (subids-extension-subgids b)))
+   (subuids (append-subid-ranges
+             (subids-extension-subuids a)
+             (subids-extension-subuids b)))))
+
+(define subids-service-type
+  (service-type (name 'subids)
+                ;; Concatenate <subid-range> lists.
+                (compose (lambda (args)
+                           (fold subids-extension-merge
+                                 (subids-extension)
+                                 args)))
+                (extend
+                 (lambda (config extension)
+                   (subids-configuration
+                    (inherit config)
+                    (subgids
+                     (append-subid-ranges
+                      (subids-configuration-subgids config)
+                      (subids-extension-subgids extension)))
+                    (subuids
+                     (append-subid-ranges
+                      (subids-configuration-subuids config)
+                      (subids-extension-subuids extension))))))
+                (extensions
+                 (list (service-extension activation-service-type
+                                          subids-activation)))
+                (default-value
+                  (subids-configuration))
+                (description
+                 "Ensure the specified sub UIDs and sub GIDs exist in
+/etc/subuid and /etc/subgid.")))
+
 ;;; shadow.scm ends here
diff --git a/gnu/tests/shadow.scm b/gnu/tests/shadow.scm
new file mode 100644
index 0000000000..849b7b8af0
--- /dev/null
+++ b/gnu/tests/shadow.scm
@@ -0,0 +1,180 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2024 Giacomo Leidi <goodoldpaul@autistici.org>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (gnu tests shadow)
+  #:use-module (gnu packages base)
+  #:use-module (gnu packages containers)
+  #:use-module (gnu tests)
+  #:use-module (gnu services)
+  #:use-module (gnu system)
+  #:use-module (gnu system accounts)
+  #:use-module (gnu system shadow)
+  #:use-module (gnu system vm)
+  #:use-module (guix gexp)
+  #:export (%test-subids))
+
+
+(define %subids-os
+  (simple-operating-system
+   (simple-service
+    'simple-profile
+    profile-service-type
+    (list podman))
+   (simple-service
+    'simple-subids
+    subids-service-type
+    (subids-extension
+     (subgids
+      (list
+       (subid-range
+        (name "alice"))
+       (subid-range
+        (name "bob")
+        (start 100700))))
+     (subuids
+      (list
+       (subid-range
+        (name "alice"))))))))
+
+(define (run-subids-test)
+  "Run IMAGE as an OCI backed Shepherd service, inside OS."
+
+  (define os
+    (marionette-operating-system
+     (operating-system-with-gc-roots
+      %subids-os
+      (list))
+     #:imported-modules '((gnu services herd)
+                          (guix combinators))))
+
+  (define vm
+    (virtual-machine
+     (operating-system os)
+     (volatile? #f)
+     (memory-size 1024)
+     (disk-image-size (* 3000 (expt 2 20)))
+     (port-forwardings '())))
+
+  (define test
+    (with-imported-modules '((gnu build marionette))
+      #~(begin
+          (use-modules (srfi srfi-11) (srfi srfi-64)
+                       (gnu build marionette))
+
+          (define marionette
+            ;; Relax timeout to accommodate older systems and
+            ;; allow for pulling the image.
+            (make-marionette (list #$vm) #:timeout 60))
+
+          (test-runner-current (system-test-runner #$output))
+          (test-begin "subids")
+
+          (test-equal "/etc/subid and /etc/subgid are created and their content is sound"
+            '("root:100000:700\nbob:100700:65536\nalice:166236:65536\n"
+              "root:100000:65536\nalice:165536:65536\n")
+            (marionette-eval
+             `(begin
+                (use-modules (ice-9 textual-ports))
+
+                (define (read-file file-name)
+                  (call-with-input-file file-name get-string-all))
+
+                (let* ((response1 (read-file "/etc/subgid"))
+                       (response2 (read-file "/etc/subuid")))
+                  (list response1 response2)))
+             marionette))
+
+          (test-equal "podman unshare runs for unprivileged users"
+            "         0       1000          1\n         1     165536      65536"
+            (marionette-eval
+             `(begin
+                (use-modules (srfi srfi-1)
+                             (ice-9 popen)
+                             (ice-9 match)
+                             (ice-9 rdelim)
+                             (ice-9 textual-ports))
+                (define out-dir "/tmp")
+                (define (read-file file-name)
+                  (call-with-input-file file-name get-string-all))
+
+                (define (wait-for-file file)
+                  ;; Wait until FILE shows up.
+                  (let loop ((i 60))
+                    (cond ((file-exists? file)
+                           #t)
+                          ((zero? i)
+                           (error "file didn't show up" file))
+                          (else
+                           (sleep 1)
+                           (loop (- i 1))))))
+
+                (define (read-lines file-or-port)
+                  (define (loop-lines port)
+                    (let loop ((lines '()))
+                      (match (read-line port)
+                        ((? eof-object?)
+                         (reverse lines))
+                        (line
+                         (loop (cons line lines))))))
+
+                  (if (port? file-or-port)
+                      (loop-lines file-or-port)
+                      (call-with-input-file file-or-port
+                        loop-lines)))
+
+                (define slurp
+                  (lambda args
+                    (let* ((port (apply open-pipe* OPEN_READ
+                                        (list "sh" "-l" "-c"
+                                              (string-join
+                                               args
+                                               " "))))
+                           (output (read-lines port))
+                           (status (close-pipe port)))
+                      output)))
+
+                (match (primitive-fork)
+                  (0
+                   (dynamic-wind
+                     (const #f)
+                     (lambda ()
+                       (setgid (passwd:gid (getpwnam "alice")))
+                       (setuid (passwd:uid (getpw "alice")))
+
+                       (let* ((response1 (slurp
+                                          "podman" "unshare" "cat" "/proc/self/uid_map")))
+                         (call-with-output-file (string-append out-dir "/response1")
+                           (lambda (port)
+                             (display (string-join response1 "\n") port)))))
+                     (lambda ()
+                       (primitive-exit 127))))
+                  (pid
+                   (cdr (waitpid pid))))
+                (wait-for-file (string-append out-dir "/response1"))
+                (read-file (string-append out-dir "/response1")))
+             marionette))
+
+          (test-end))))
+
+  (gexp->derivation "subids-test" test))
+
+(define %test-subids
+  (system-test
+   (name "subids")
+   (description "Test sub UIDs and sub GIDs provisioning service.")
+   (value (run-subids-test))))
