diff --git a/lib/modules/datasource/metadata.ts b/lib/modules/datasource/metadata.ts
index 8f6fd5fc56f5cf6724c32c56a58e8080cbd80665..bdfbe258b026454a1e6024c848f7776196848a87 100644
--- a/lib/modules/datasource/metadata.ts
+++ b/lib/modules/datasource/metadata.ts
@@ -64,6 +64,9 @@ function massageGitAtUrl(url: string): string {
   return massagedUrl;
 }
 
+/**
+ * @deprecated Use `asTimestamp` instead
+ */
 export function normalizeDate(input: any): string | null {
   if (
     typeof input === 'number' &&
diff --git a/lib/util/timestamp.spec.ts b/lib/util/timestamp.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c084c062f3025ededb6e188ec499ec4d014e0e53
--- /dev/null
+++ b/lib/util/timestamp.spec.ts
@@ -0,0 +1,39 @@
+import { TimestampSchema, asTimestamp } from './timestamp';
+
+describe('util/timestamp', () => {
+  describe('asTimestamp', () => {
+    test.each`
+      input                                        | expected
+      ${new Date('2021-01-01T00:00:00.000Z')}      | ${'2021-01-01T00:00:00.000Z'}
+      ${new Date('2021-01-01T00:00:00.000-03:00')} | ${'2021-01-01T03:00:00.000Z'}
+      ${new Date('1999-01-01T00:00:00.000Z')}      | ${null}
+      ${1609459200000}                             | ${'2021-01-01T00:00:00.000Z'}
+      ${1609459200}                                | ${'2021-01-01T00:00:00.000Z'}
+      ${-1}                                        | ${null}
+      ${0}                                         | ${null}
+      ${123}                                       | ${null}
+      ${NaN}                                       | ${null}
+      ${'2021-01-01T00:00:00.000Z'}                | ${'2021-01-01T00:00:00.000Z'}
+      ${'2021-01-01'}                              | ${'2021-01-01T00:00:00.000Z'}
+      ${'20210101000000'}                          | ${'2021-01-01T00:00:00.000Z'}
+      ${'20211231235959'}                          | ${'2021-12-31T23:59:59.000Z'}
+      ${'Jan 1, 2021'}                             | ${'2021-01-01T00:00:00.000Z'}
+      ${'2021/01/01'}                              | ${'2021-01-01T00:00:00.000Z'}
+      ${'2021-01-02T00:00:00+05:30'}               | ${'2021-01-01T18:30:00.000Z'}
+      ${'2010-05-20T22:43:19-07:00'}               | ${'2010-05-21T05:43:19.000Z'}
+      ${'2021-10-11 07:47:24 -0700'}               | ${'2021-10-11T14:47:24.000Z'}
+      ${'Wed, 21 Oct 2015 07:28:00 GMT'}           | ${'2015-10-21T07:28:00.000Z'}
+      ${null}                                      | ${null}
+      ${undefined}                                 | ${null}
+      ${{}}                                        | ${null}
+      ${[]}                                        | ${null}
+      ${'invalid date'}                            | ${null}
+      ${'202x0101000000'}                          | ${null}
+    `('$input -> $expected', ({ input, expected }) => {
+      expect(asTimestamp(input)).toBe(expected);
+      expect(TimestampSchema.nullable().catch(null).parse(input)).toBe(
+        expected,
+      );
+    });
+  });
+});
diff --git a/lib/util/timestamp.ts b/lib/util/timestamp.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b40fd93ead2d2a7cf346f8ddbd78d09682159d04
--- /dev/null
+++ b/lib/util/timestamp.ts
@@ -0,0 +1,92 @@
+import { DateTime } from 'luxon';
+import { z } from 'zod';
+
+export type Timestamp = string & { __timestamp: never };
+
+const timezoneOffset = new Date().getTimezoneOffset() * 60000;
+
+const millenium = 946684800000; // 2000-01-01T00:00:00.000Z
+const tomorrowOffset = 86400000; // 24 * 60 * 60 * 1000;
+
+function isValid(date: DateTime): boolean {
+  if (!date.isValid) {
+    return false;
+  }
+  const tomorrow = DateTime.now().toMillis() + tomorrowOffset; // 24 * 60 * 60 * 1000;
+  const ts = date.toMillis();
+  return ts > millenium && ts < tomorrow;
+}
+
+export function asTimestamp(input: unknown): Timestamp | null {
+  if (input instanceof Date) {
+    const date = DateTime.fromJSDate(input, { zone: 'UTC' });
+    if (isValid(date)) {
+      return date.toISO() as Timestamp;
+    }
+
+    return null;
+  }
+
+  if (typeof input === 'number') {
+    const millisDate = DateTime.fromMillis(input, { zone: 'UTC' });
+    if (isValid(millisDate)) {
+      return millisDate.toISO() as Timestamp;
+    }
+
+    const secondsDate = DateTime.fromSeconds(input, { zone: 'UTC' });
+    if (isValid(secondsDate)) {
+      return secondsDate.toISO() as Timestamp;
+    }
+
+    return null;
+  }
+
+  if (typeof input === 'string') {
+    const isoDate = DateTime.fromISO(input, { zone: 'UTC' });
+    if (isValid(isoDate)) {
+      return isoDate.toISO() as Timestamp;
+    }
+
+    const httpDate = DateTime.fromHTTP(input, { zone: 'UTC' });
+    if (isValid(httpDate)) {
+      return httpDate.toISO() as Timestamp;
+    }
+
+    const sqlDate = DateTime.fromSQL(input, { zone: 'UTC' });
+    if (isValid(sqlDate)) {
+      return sqlDate.toISO() as Timestamp;
+    }
+
+    const numberLikeDate = DateTime.fromFormat(input, 'yyyyMMddHHmmss', {
+      zone: 'UTC',
+    });
+    if (isValid(numberLikeDate)) {
+      return numberLikeDate.toISO() as Timestamp;
+    }
+
+    const fallbackDate = DateTime.fromMillis(
+      Date.parse(input) - timezoneOffset,
+      { zone: 'UTC' },
+    );
+    if (isValid(fallbackDate)) {
+      return fallbackDate.toISO() as Timestamp;
+    }
+
+    return null;
+  }
+
+  return null;
+}
+
+export const TimestampSchema = z.unknown().transform((input, ctx) => {
+  const timestamp = asTimestamp(input);
+  if (!timestamp) {
+    ctx.addIssue({
+      code: z.ZodIssueCode.custom,
+      message: 'Invalid timestamp',
+    });
+    return z.NEVER;
+  }
+
+  return timestamp;
+});