Fix flatten push auth by retargeting short names to vault registry
Release rolling / release (push) Has been cancelled

Private registry credentials were applied while pushing Docker Hub short
names. Auto-retag under the credential host and prefix the flattener repo.
This commit is contained in:
Raven Scott
2026-07-18 07:46:07 -04:00
parent e9fb26153a
commit 8235a8a6c8
8 changed files with 310 additions and 7 deletions
+41 -1
View File
@@ -4,7 +4,16 @@
import { docker } from '../services/docker.js'
import * as validation from '../utils/validation.js'
import { Pushes } from '../../shared/protocol.js'
import { resolveRegistryAuth, isUsableDockerAuth } from './vault.js'
import {
resolveRegistryAuth,
isUsableDockerAuth,
authMatchesImage,
retargetImageRefForRegistry,
parseImageRepoTag,
registryPrefixFromServeraddress,
normalizeRegistryHost,
registryHostFromImage,
} from './vault.js'
import logger from '../utils/logger.js'
/**
@@ -318,6 +327,7 @@ export function registerImageHandlers(session) {
let pushRef = imageName
/** Explicit tag query only when set — never pass undefined (see below). */
let pushTag = null
let retargeted = false
if (args.repo) {
const repo = validation.sanitizeString(args.repo, 255)
const tag = validation.sanitizeString(args.tag || 'latest', 128)
@@ -338,6 +348,35 @@ export function registerImageHandlers(session) {
image: pushRef,
})
// Short name + private registry credential → auto-retag under that registry.
// Otherwise Docker pushes to docker.io with the private username/password → 401.
if (authconfig && !authMatchesImage(authconfig, pushRef)) {
const desired = retargetImageRefForRegistry(pushRef, authconfig.serveraddress)
if (desired !== pushRef) {
const { repo, tag } = parseImageRepoTag(desired)
await docker.getImage(args.id || imageName || pushRef).tag({ repo, tag })
logger.info('pushImage: retagged short name for credential registry', {
from: pushRef,
to: desired,
serveraddress: authconfig.serveraddress,
})
pushRef = desired
pushTag = null
retargeted = true
} else {
const authHost = normalizeRegistryHost(authconfig.serveraddress)
const imageHost = registryHostFromImage(pushRef) || 'unknown'
const prefix = registryPrefixFromServeraddress(authconfig.serveraddress)
throw Object.assign(
new Error(
`Credential is for registry "${authHost}" but image "${pushRef}" targets "${imageHost}". ` +
`Retag to ${prefix ? `${prefix}/` : ''}your/image:tag before pushing, or pick a matching credential.`
),
{ code: 'REGISTRY_HOST_MISMATCH' }
)
}
}
const pushOpts = buildImagePushOpts({ tag: pushTag, authconfig })
const image = docker.getImage(pushRef)
const stream = await image.push(pushOpts)
@@ -378,6 +417,7 @@ export function registerImageHandlers(session) {
message: `Image "${pushRef}" pushed`,
image: pushRef,
usedAuth: Boolean(authconfig),
retargeted,
}
})
+76
View File
@@ -81,6 +81,82 @@ export function authMatchesImage(auth, image) {
return imageHost === authHost
}
/**
* Host[:port] prefix for tagging images toward a vault serveraddress.
* Empty string for Docker Hub (short names push to docker.io).
* @param {string} [serveraddress]
* @returns {string}
*/
export function registryPrefixFromServeraddress(serveraddress) {
let raw = String(serveraddress || '')
.toLowerCase()
.trim()
.replace(/^https?:\/\//, '')
.replace(/\/+$/, '')
// Strip API path suffixes commonly stored on credentials
raw = raw.replace(/\/v2\/?$/i, '').replace(/\/v1\/?$/i, '')
const hostPort = raw.split('/')[0] || ''
if (
!hostPort ||
hostPort === 'docker.io' ||
hostPort === 'index.docker.io' ||
hostPort === 'registry-1.docker.io' ||
hostPort === 'registry.hub.docker.com' ||
hostPort.includes('docker.io')
) {
return ''
}
return hostPort
}
/**
* Split image ref into { repo, tag }.
* @param {string} ref
* @returns {{ repo: string, tag: string }}
*/
export function parseImageRepoTag(ref) {
const s = String(ref || '').trim()
if (!s) return { repo: '', tag: 'latest' }
const at = s.indexOf('@')
const noDigest = at >= 0 ? s.slice(0, at) : s
const lastSlash = noDigest.lastIndexOf('/')
const lastColon = noDigest.lastIndexOf(':')
if (lastColon > lastSlash) {
return {
repo: noDigest.slice(0, lastColon),
tag: noDigest.slice(lastColon + 1) || 'latest',
}
}
return { repo: noDigest, tag: 'latest' }
}
/**
* If image is a short (docker.io) name and auth targets a private registry,
* return the retagged destination `registry[:port]/name:tag`. Otherwise return ref unchanged.
* @param {string} imageRef
* @param {string} [serveraddress]
* @returns {string}
*/
export function retargetImageRefForRegistry(imageRef, serveraddress) {
const ref = String(imageRef || '').trim()
if (!ref) return ref
const prefix = registryPrefixFromServeraddress(serveraddress)
if (!prefix) return ref
const imageHost = registryHostFromImage(ref)
// Already points at a non-Hub registry — leave as-is (caller may error on mismatch)
if (imageHost && imageHost !== 'docker.io') return ref
const { repo, tag } = parseImageRepoTag(ref)
let name = repo
if (name.startsWith('docker.io/')) name = name.slice('docker.io/'.length)
if (name.startsWith('library/')) name = name.slice('library/'.length)
if (!name) return ref
// Avoid double-prefix if user already typed the host without it looking like a registry
if (name === prefix || name.startsWith(`${prefix}/`)) return `${name}:${tag}`
return `${prefix}/${name}:${tag}`
}
/**
* Resolve Docker authconfig for pull/push from vault id, inline auth, or session.
* Returns null for anonymous (no usable credentials for that registry).