diff --git a/docs/usage/getting-started/private-packages.md b/docs/usage/getting-started/private-packages.md
index b4d60f24dfd9ea34e52b9c580082f0cd259efc40..11804223a52e223c6260d57af9ae8f29205691d1 100644
--- a/docs/usage/getting-started/private-packages.md
+++ b/docs/usage/getting-started/private-packages.md
@@ -325,7 +325,35 @@ For those found, a command similar to the following is run: `dotnet nuget add so
 
 ### poetry
 
-For every poetry source, a `hostRules` search is done and then any found credentials are added to env like `POETRY_HTTP_BASIC_X_USERNAME` and `POETRY_HTTP_BASIC_X_PASSWORD`.
+For every Poetry source, a `hostRules` search is done and then any found credentials are added to env like `POETRY_HTTP_BASIC_X_USERNAME` and `POETRY_HTTP_BASIC_X_PASSWORD`, where `X` represents the normalized name of the source in `pyproject.toml`.
+
+```js
+module.exports = {
+  hostRules: [
+    {
+      matchHost: 'pypi.example.com',
+      hostType: 'pypi',
+      username: process.env.PYPI_USERNAME,
+      password: process.env.PYPI_PASSWORD,
+    },
+  ],
+};
+```
+
+If you're self-hosting Renovate via the [GitLab Runner](../getting-started/running.md#gitlab-runner) and want to access packages from private GitLab registries, you can use the GitLab CI job token for authentication:
+
+```js
+module.exports = {
+  hostRules: [
+    {
+      matchHost: 'gitlab.example.com',
+      hostType: 'pypi',
+      username: 'gitlab-ci-token',
+      password: process.env.CI_JOB_TOKEN,
+    },
+  ],
+};
+```
 
 ## WhiteSource Renovate Hosted App Encryption
 
diff --git a/lib/manager/poetry/__fixtures__/pyproject.10.toml b/lib/manager/poetry/__fixtures__/pyproject.10.toml
index 3b2499c44b0379c81f124ddaa75086c85a891aec..14c589fab7736df2c332883be90d1d017f0975e4 100644
--- a/lib/manager/poetry/__fixtures__/pyproject.10.toml
+++ b/lib/manager/poetry/__fixtures__/pyproject.10.toml
@@ -10,7 +10,7 @@ url = "another.url"
 url = "missingname.url"
 
 [[tool.poetry.source]]
-name = "four"
+name = "four-oh.four"
 url = "last.url"
 
 [[tool.poetry.source]]
diff --git a/lib/manager/poetry/__snapshots__/artifacts.spec.ts.snap b/lib/manager/poetry/__snapshots__/artifacts.spec.ts.snap
index 89b376fad158ddf8a906d86009f9fae9c8df000d..eba67540534e64ad358db06742ce910d28ae4201 100644
--- a/lib/manager/poetry/__snapshots__/artifacts.spec.ts.snap
+++ b/lib/manager/poetry/__snapshots__/artifacts.spec.ts.snap
@@ -27,7 +27,7 @@ Array [
         "LC_ALL": "en_US",
         "NO_PROXY": "localhost",
         "PATH": "/tmp/path",
-        "POETRY_HTTP_BASIC_FOUR_PASSWORD": "passwordFour",
+        "POETRY_HTTP_BASIC_FOUR_OH_FOUR_PASSWORD": "passwordFour",
         "POETRY_HTTP_BASIC_ONE_PASSWORD": "passwordOne",
         "POETRY_HTTP_BASIC_ONE_USERNAME": "usernameOne",
         "POETRY_HTTP_BASIC_TWO_USERNAME": "usernameTwo",
@@ -39,6 +39,30 @@ Array [
 ]
 `;
 
+exports[`manager/poetry/artifacts prioritizes pypi-scoped credentials 1`] = `
+Array [
+  Object {
+    "cmd": "poetry update --lock --no-interaction dep1",
+    "options": Object {
+      "cwd": "/tmp/github/some/repo",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+        "POETRY_HTTP_BASIC_ONE_PASSWORD": "scoped-password",
+      },
+      "maxBuffer": 10485760,
+      "timeout": 900000,
+    },
+  },
+]
+`;
+
 exports[`manager/poetry/artifacts returns null if unchanged 1`] = `
 Array [
   Object {
diff --git a/lib/manager/poetry/artifacts.spec.ts b/lib/manager/poetry/artifacts.spec.ts
index 07e2cc5fff31b69a9acf413eb6d412713fc83625..6dcbb618d8b6607249e4b15d3d49742abf5aa117 100644
--- a/lib/manager/poetry/artifacts.spec.ts
+++ b/lib/manager/poetry/artifacts.spec.ts
@@ -101,6 +101,7 @@ describe('manager/poetry/artifacts', () => {
       password: 'passwordOne',
     });
     hostRules.find.mockReturnValueOnce({ username: 'usernameTwo' });
+    hostRules.find.mockReturnValueOnce({});
     hostRules.find.mockReturnValueOnce({ password: 'passwordFour' });
     const updatedDeps = [{ depName: 'dep1' }];
     expect(
@@ -111,7 +112,31 @@ describe('manager/poetry/artifacts', () => {
         config,
       })
     ).not.toBeNull();
-    expect(hostRules.find.mock.calls).toHaveLength(3);
+    expect(hostRules.find.mock.calls).toHaveLength(4);
+    expect(execSnapshots).toMatchSnapshot();
+  });
+  it('prioritizes pypi-scoped credentials', async () => {
+    fs.readFile.mockResolvedValueOnce(null);
+    fs.readFile.mockResolvedValueOnce(Buffer.from('[metadata]\n'));
+    const execSnapshots = mockExecAll(exec);
+    fs.readFile.mockResolvedValueOnce(Buffer.from('New poetry.lock'));
+    hostRules.find.mockImplementation((search) => ({
+      password:
+        search.hostType === 'pypi' ? 'scoped-password' : 'unscoped-password',
+    }));
+    const updatedDeps = [{ depName: 'dep1' }];
+    expect(
+      await updateArtifacts({
+        packageFileName: 'pyproject.toml',
+        updatedDeps,
+        newPackageFileContent: `
+          [[tool.poetry.source]]
+          name = "one"
+          url = "some.url"
+        `,
+        config,
+      })
+    ).not.toBeNull();
     expect(execSnapshots).toMatchSnapshot();
   });
   it('returns updated pyproject.lock', async () => {
diff --git a/lib/manager/poetry/artifacts.ts b/lib/manager/poetry/artifacts.ts
index 69ad411f812351f723689d6cbb475160096d923f..21e63611e5a46e347df6d8133b7b84084e683c30 100644
--- a/lib/manager/poetry/artifacts.ts
+++ b/lib/manager/poetry/artifacts.ts
@@ -2,7 +2,9 @@ import { parse } from '@iarna/toml';
 import is from '@sindresorhus/is';
 import { quote } from 'shlex';
 import { TEMPORARY_ERROR } from '../../constants/error-messages';
+import { PypiDatasource } from '../../datasource/pypi';
 import { logger } from '../../logger';
+import type { HostRule } from '../../types';
 import { exec } from '../../util/exec';
 import type { ExecOptions, ToolConstraint } from '../../util/exec/types';
 import {
@@ -99,6 +101,13 @@ function getPoetrySources(content: string, fileName: string): PoetrySource[] {
   return sourceArray;
 }
 
+function getMatchingHostRule(source: PoetrySource): HostRule {
+  const scopedMatch = find({ hostType: PypiDatasource.id, url: source.url });
+  return is.nonEmptyObject(scopedMatch)
+    ? scopedMatch
+    : find({ url: source.url });
+}
+
 function getSourceCredentialVars(
   pyprojectContent: string,
   packageFileName: string
@@ -107,8 +116,10 @@ function getSourceCredentialVars(
   const envVars: Record<string, string> = {};
 
   for (const source of poetrySources) {
-    const matchingHostRule = find({ url: source.url });
-    const formattedSourceName = source.name.toUpperCase();
+    const matchingHostRule = getMatchingHostRule(source);
+    const formattedSourceName = source.name
+      .replace(regEx(/(\.|-)+/g), '_')
+      .toUpperCase();
     if (matchingHostRule.username) {
       envVars[`POETRY_HTTP_BASIC_${formattedSourceName}_USERNAME`] =
         matchingHostRule.username;