diff --git a/NEWS.md b/NEWS.md
index 824d730ed3c3c..39ddd762bd054 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -51,6 +51,8 @@ Standard library changes
 
 #### Random
 
+* When seeding RNGs provided by `Random`, negative integer seeds can now be used ([#51416]).
+
 #### REPL
 
 * Tab complete hints now show in lighter text while typing in the repl. To disable
diff --git a/stdlib/Random/src/RNGs.jl b/stdlib/Random/src/RNGs.jl
index 272b46f9774d9..843386d5450f3 100644
--- a/stdlib/Random/src/RNGs.jl
+++ b/stdlib/Random/src/RNGs.jl
@@ -83,12 +83,12 @@ MersenneTwister(seed::Vector{UInt32}, state::DSFMT_state) =
 Create a `MersenneTwister` RNG object. Different RNG objects can have
 their own seeds, which may be useful for generating different streams
 of random numbers.
-The `seed` may be a non-negative integer or a vector of
-`UInt32` integers. If no seed is provided, a randomly generated one
-is created (using entropy from the system).
-See the [`seed!`](@ref) function for reseeding an already existing
-`MersenneTwister` object.
+The `seed` may be an integer or a vector of `UInt32` integers.
+If no seed is provided, a randomly generated one is created (using entropy from the system).
+See the [`seed!`](@ref) function for reseeding an already existing `MersenneTwister` object.
 
+!!! compat "Julia 1.11"
+    Passing a negative integer seed requires at least Julia 1.11.
 
 # Examples
 ```jldoctest
@@ -290,20 +290,48 @@ function make_seed()
     end
 end
 
+"""
+    make_seed(n::Integer) -> Vector{UInt32}
+
+Transform `n` into a bit pattern encoded as a `Vector{UInt32}`, suitable for
+RNG seeding routines.
+
+`make_seed` is "injective" : if `n != m`, then `make_seed(n) != `make_seed(m)`.
+Moreover, if `n == m`, then `make_seed(n) == make_seed(m)`.
+
+This is an internal function, subject to change.
+"""
 function make_seed(n::Integer)
-    n < 0 && throw(DomainError(n, "`n` must be non-negative."))
+    neg = signbit(n)
+    if neg
+        n = ~n
+    end
+    @assert n >= 0
     seed = UInt32[]
-    while true
+    # we directly encode the bit pattern of `n` into the resulting vector `seed`;
+    # to greatly limit breaking the streams of random numbers, we encode the sign bit
+    # as the upper bit of `seed[end]` (i.e. for most positive seeds, `make_seed` returns
+    # the same vector as when we didn't encode the sign bit)
+    while !iszero(n)
         push!(seed, n & 0xffffffff)
-        n >>= 32
-        if n == 0
-            return seed
-        end
+        n >>>= 32
+    end
+    if isempty(seed) || !iszero(seed[end] & 0x80000000)
+        push!(seed, zero(UInt32))
     end
+    if neg
+        seed[end] |= 0x80000000
+    end
+    seed
 end
 
 # inverse of make_seed(::Integer)
-from_seed(a::Vector{UInt32})::BigInt = sum(a[i] * big(2)^(32*(i-1)) for i in 1:length(a))
+function from_seed(a::Vector{UInt32})::BigInt
+    neg = !iszero(a[end] & 0x80000000)
+    seed = sum((i == length(a) ? a[i] & 0x7fffffff : a[i]) * big(2)^(32*(i-1))
+               for i in 1:length(a))
+    neg ? ~seed : seed
+end
 
 
 #### seed!()
diff --git a/stdlib/Random/src/Random.jl b/stdlib/Random/src/Random.jl
index 78d4f15e2beac..15165f5380945 100644
--- a/stdlib/Random/src/Random.jl
+++ b/stdlib/Random/src/Random.jl
@@ -394,6 +394,8 @@ sequence of numbers if and only if a `seed` is provided. Some RNGs
 don't accept a seed, like `RandomDevice`.
 After the call to `seed!`, `rng` is equivalent to a newly created
 object initialized with the same seed.
+The types of accepted seeds depend on the type of `rng`, but in general,
+integer seeds should work.
 
 If `rng` is not specified, it defaults to seeding the state of the
 shared task-local generator.
diff --git a/stdlib/Random/src/Xoshiro.jl b/stdlib/Random/src/Xoshiro.jl
index 5a03e2b84536d..feb26da3101ac 100644
--- a/stdlib/Random/src/Xoshiro.jl
+++ b/stdlib/Random/src/Xoshiro.jl
@@ -4,7 +4,7 @@
 # Lots of implementation is shared with TaskLocalRNG
 
 """
-    Xoshiro(seed)
+    Xoshiro(seed::Integer)
     Xoshiro()
 
 Xoshiro256++ is a fast pseudorandom number generator described by David Blackman and
@@ -21,6 +21,12 @@ multiple interleaved xoshiro instances).
 The virtual PRNGs are discarded once the bulk request has been serviced (and should cause
 no heap allocations).
 
+If no seed is provided, a randomly generated one is created (using entropy from the system).
+See the [`seed!`](@ref) function for reseeding an already existing `Xoshiro` object.
+
+!!! compat "Julia 1.11"
+    Passing a negative integer seed requires at least Julia 1.11.
+
 # Examples
 ```jldoctest
 julia> using Random
@@ -191,6 +197,12 @@ endianness and possibly word size.
 
 Using or seeding the RNG of any other task than the one returned by `current_task()`
 is undefined behavior: it will work most of the time, and may sometimes fail silently.
+
+When seeding `TaskLocalRNG()` with [`seed!`](@ref), the passed seed, if any,
+may be any integer.
+
+!!! compat "Julia 1.11"
+    Seeding `TaskLocalRNG()` with a negative integer seed requires at least Julia 1.11.
 """
 struct TaskLocalRNG <: AbstractRNG end
 TaskLocalRNG(::Nothing) = TaskLocalRNG()
diff --git a/stdlib/Random/test/runtests.jl b/stdlib/Random/test/runtests.jl
index 462a9f86cfecf..836d7aaa68ab9 100644
--- a/stdlib/Random/test/runtests.jl
+++ b/stdlib/Random/test/runtests.jl
@@ -926,6 +926,12 @@ end
         m = MersenneTwister(0); rand(m, Int64); rand(m)
         @test string(m) == "MersenneTwister(0, (0, 2256, 1254, 1, 0, 1))"
         @test m == MersenneTwister(0, (0, 2256, 1254, 1, 0, 1))
+
+        # negative seeds
+        Random.seed!(m, -3)
+        @test string(m) == "MersenneTwister(-3)"
+        Random.seed!(m, typemin(Int8))
+        @test string(m) == "MersenneTwister(-128)"
     end
 
     @testset "RandomDevice" begin
@@ -1148,3 +1154,33 @@ end
         end
     end
 end
+
+@testset "seed! and make_seed" begin
+    # Test that:
+    # 1) if n == m, then make_seed(n) == make_seed(m)
+    # 2) if n != m, then make_seed(n) != make_seed(m)
+    rngs = (Xoshiro(0), TaskLocalRNG(), MersenneTwister(0))
+    seeds = Any[]
+    for T = Base.BitInteger_types
+        append!(seeds, rand(T, 8))
+        push!(seeds, typemin(T), typemin(T) + T(1), typemin(T) + T(2),
+              typemax(T), typemax(T) - T(1), typemax(T) - T(2))
+        T <: Signed && push!(seeds, T(0), T(1), T(2), T(-1), T(-2))
+    end
+
+    vseeds = Dict{Vector{UInt32}, BigInt}()
+    for seed = seeds
+        bigseed = big(seed)
+        vseed = Random.make_seed(bigseed)
+        # test property 1) above
+        @test Random.make_seed(seed) == vseed
+        # test property 2) above
+        @test bigseed == get!(vseeds, vseed, bigseed)
+        # test that the property 1) is actually inherited by `seed!`
+        for rng = rngs
+            rng2 = copy(Random.seed!(rng, seed))
+            Random.seed!(rng, bigseed)
+            @test rng == rng2
+        end
+    end
+end