diff --git a/models/user/user.go b/models/user/user.go
index 0a43de74354..7e896e26dab 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -640,6 +640,11 @@ func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err e
 	u.IsRestricted = setting.Service.DefaultUserIsRestricted
 	u.IsActive = !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm)
 
+	// Ensure consistency of the dates.
+	if u.UpdatedUnix < u.CreatedUnix {
+		u.UpdatedUnix = u.CreatedUnix
+	}
+
 	// overwrite defaults if set
 	if len(overwriteDefault) != 0 && overwriteDefault[0] != nil {
 		overwrite := overwriteDefault[0]
@@ -717,7 +722,15 @@ func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err e
 		return err
 	}
 
-	if err = db.Insert(ctx, u); err != nil {
+	if u.CreatedUnix == 0 {
+		// Caller expects auto-time for creation & update timestamps.
+		err = db.Insert(ctx, u)
+	} else {
+		// Caller sets the timestamps themselves. They are responsible for ensuring
+		// both `CreatedUnix` and `UpdatedUnix` are set appropriately.
+		_, err = db.GetEngine(ctx).NoAutoTime().Insert(u)
+	}
+	if err != nil {
 		return err
 	}
 
diff --git a/models/user/user_test.go b/models/user/user_test.go
index 525da531f23..7a58d2f822c 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -4,9 +4,11 @@
 package user_test
 
 import (
+	"context"
 	"math/rand"
 	"strings"
 	"testing"
+	"time"
 
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
@@ -14,6 +16,7 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
 	"github.com/stretchr/testify/assert"
@@ -252,6 +255,58 @@ func TestCreateUserEmailAlreadyUsed(t *testing.T) {
 	assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
 }
 
+func TestCreateUserCustomTimestamps(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+	// Add new user with a custom creation timestamp.
+	var creationTimestamp timeutil.TimeStamp = 12345
+	user.Name = "testuser"
+	user.LowerName = strings.ToLower(user.Name)
+	user.ID = 0
+	user.Email = "unique@example.com"
+	user.CreatedUnix = creationTimestamp
+	err := user_model.CreateUser(user)
+	assert.NoError(t, err)
+
+	fetched, err := user_model.GetUserByID(context.Background(), user.ID)
+	assert.NoError(t, err)
+	assert.Equal(t, creationTimestamp, fetched.CreatedUnix)
+	assert.Equal(t, creationTimestamp, fetched.UpdatedUnix)
+}
+
+func TestCreateUserWithoutCustomTimestamps(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+	// There is no way to use a mocked time for the XORM auto-time functionality,
+	// so use the real clock to approximate the expected timestamp.
+	timestampStart := time.Now().Unix()
+
+	// Add new user without a custom creation timestamp.
+	user.Name = "Testuser"
+	user.LowerName = strings.ToLower(user.Name)
+	user.ID = 0
+	user.Email = "unique@example.com"
+	user.CreatedUnix = 0
+	user.UpdatedUnix = 0
+	err := user_model.CreateUser(user)
+	assert.NoError(t, err)
+
+	timestampEnd := time.Now().Unix()
+
+	fetched, err := user_model.GetUserByID(context.Background(), user.ID)
+	assert.NoError(t, err)
+
+	assert.LessOrEqual(t, timestampStart, fetched.CreatedUnix)
+	assert.LessOrEqual(t, fetched.CreatedUnix, timestampEnd)
+
+	assert.LessOrEqual(t, timestampStart, fetched.UpdatedUnix)
+	assert.LessOrEqual(t, fetched.UpdatedUnix, timestampEnd)
+}
+
 func TestGetUserIDsByNames(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
diff --git a/modules/structs/admin_user.go b/modules/structs/admin_user.go
index 0739653eea4..4d679c81d00 100644
--- a/modules/structs/admin_user.go
+++ b/modules/structs/admin_user.go
@@ -4,6 +4,8 @@
 
 package structs
 
+import "time"
+
 // CreateUserOption create user options
 type CreateUserOption struct {
 	SourceID  int64  `json:"source_id"`
@@ -20,6 +22,11 @@ type CreateUserOption struct {
 	SendNotify         bool   `json:"send_notify"`
 	Restricted         *bool  `json:"restricted"`
 	Visibility         string `json:"visibility" binding:"In(,public,limited,private)"`
+
+	// For explicitly setting the user creation timestamp. Useful when users are
+	// migrated from other systems. When omitted, the user's creation timestamp
+	// will be set to "now".
+	Created *time.Time `json:"created_at"`
 }
 
 // EditUserOption edit user options
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
index 4ee1a320ccb..75d5520a0ee 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -20,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/password"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/user"
@@ -120,6 +121,14 @@ func CreateUser(ctx *context.APIContext) {
 		overwriteDefault.Visibility = &visibility
 	}
 
+	// Update the user creation timestamp. This can only be done after the user
+	// record has been inserted into the database; the insert intself will always
+	// set the creation timestamp to "now".
+	if form.Created != nil {
+		u.CreatedUnix = timeutil.TimeStamp(form.Created.Unix())
+		u.UpdatedUnix = u.CreatedUnix
+	}
+
 	if err := user_model.CreateUser(u, overwriteDefault); err != nil {
 		if user_model.IsErrUserAlreadyExist(err) ||
 			user_model.IsErrEmailAlreadyUsed(err) ||
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index e096faf3f3b..00fc3b60c45 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -15809,6 +15809,12 @@
         "password"
       ],
       "properties": {
+        "created_at": {
+          "description": "For explicitly setting the user creation timestamp. Useful when users are\nmigrated from other systems. When omitted, the user's creation timestamp\nwill be set to \"now\".",
+          "type": "string",
+          "format": "date-time",
+          "x-go-name": "Created"
+        },
         "email": {
           "type": "string",
           "format": "email",