Log in to artifactapi with a Vault identity OIDC token #2
@@ -1,6 +1,9 @@
|
|||||||
FROM docker.io/woodpeckerci/plugin-docker-buildx:latest
|
FROM docker.io/woodpeckerci/plugin-docker-buildx:latest
|
||||||
|
RUN apk add --no-cache curl jq
|
||||||
# buildkit reads this to trust artifactapi's internal-CA registry on push
|
# buildkit reads this to trust artifactapi's internal-CA registry on push
|
||||||
COPY unkin-ca.crt /etc/docker/certs.d/artifactapi.k8s.syd1.au.unkin.net/ca.crt
|
COPY unkin-ca.crt /etc/docker/certs.d/artifactapi.k8s.syd1.au.unkin.net/ca.crt
|
||||||
# fallback: system trust store (docker login / other code paths)
|
# fallback: system trust store (docker login / other code paths)
|
||||||
COPY unkin-ca.crt /usr/local/share/ca-certificates/unkin-ca.crt
|
COPY unkin-ca.crt /usr/local/share/ca-certificates/unkin-ca.crt
|
||||||
RUN cat /usr/local/share/ca-certificates/unkin-ca.crt >> /etc/ssl/certs/ca-certificates.crt || true
|
RUN cat /usr/local/share/ca-certificates/unkin-ca.crt >> /etc/ssl/certs/ca-certificates.crt || true
|
||||||
|
COPY artifactapi-login.sh /usr/local/bin/artifactapi-login.sh
|
||||||
|
ENTRYPOINT ["/usr/local/bin/artifactapi-login.sh"]
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
IMAGE ?= plugin-docker-buildx:test
|
||||||
|
|
||||||
|
.PHONY: build test
|
||||||
|
|
||||||
|
build:
|
||||||
|
docker build -t $(IMAGE) .
|
||||||
|
|
||||||
|
test: build
|
||||||
|
docker run --rm -v $(CURDIR):/src:ro,z --entrypoint sh $(IMAGE) /src/test/login_test.sh
|
||||||
|
test "$$(docker run --rm -v $(CURDIR)/test/stub-entrypoint.sh:/usr/local/bin/dockerd-entrypoint.sh:ro,z $(IMAGE) a 'b c')" = "plugin-docker-buildx|a|b c"
|
||||||
@@ -28,3 +28,14 @@ its own push therefore cannot rely on that trust yet.
|
|||||||
|
|
||||||
Once published, point Woodpecker docker-buildx steps at this image instead of the
|
Once published, point Woodpecker docker-buildx steps at this image instead of the
|
||||||
upstream one to push to the docker-internal registry.
|
upstream one to push to the docker-internal registry.
|
||||||
|
|
||||||
|
## artifactapi login
|
||||||
|
|
||||||
|
When pushing to `artifactapi.k8s.syd1.au.unkin.net` with no `username`/`password`
|
||||||
|
or matching `logins` entry, the entrypoint logs in to Vault with the pod's service
|
||||||
|
account token (`auth/k8s/au/syd1/login`, role `woodpecker_artifactapi_writer`),
|
||||||
|
reads `identity/oidc/token/artifactapi-writer` and adds it as the registry password.
|
||||||
|
Override with `vault_addr`, `vault_role` and `vault_oidc_role`. Missing SA token or
|
||||||
|
a Vault error logs a warning and pushes unauthenticated.
|
||||||
|
|
||||||
|
Run `make test` to exercise the login logic inside the built image.
|
||||||
|
|||||||
Executable
+37
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ARTIFACTAPI_HOST=artifactapi.k8s.syd1.au.unkin.net
|
||||||
|
|
||||||
|
warn() { echo "artifactapi-login: $*, pushing unauthenticated" >&2; }
|
||||||
|
|
||||||
|
artifactapi_login() {
|
||||||
|
[ "${PLUGIN_DRY_RUN:-false}" != true ] || return 0
|
||||||
|
case "${PLUGIN_REGISTRY:-} ${PLUGIN_REPO:-}" in *"$ARTIFACTAPI_HOST"*) ;; *) return 0 ;; esac
|
||||||
|
case "${PLUGIN_REGISTRY:-}" in *"$ARTIFACTAPI_HOST"*) [ -z "${PLUGIN_USERNAME:-}${PLUGIN_PASSWORD:-}" ] || return 0 ;; esac
|
||||||
|
case "${PLUGIN_LOGINS:-}" in *"$ARTIFACTAPI_HOST"*) return 0 ;; esac
|
||||||
|
|
||||||
|
sa=${SA_TOKEN_FILE:-/var/run/secrets/kubernetes.io/serviceaccount/token}
|
||||||
|
[ -r "$sa" ] || { warn "no service account token at $sa"; return 0; }
|
||||||
|
vault=${PLUGIN_VAULT_ADDR:-${VAULT_ADDR:-https://vault.service.consul:8200}}
|
||||||
|
role=${PLUGIN_VAULT_ROLE:-woodpecker_artifactapi_writer}
|
||||||
|
oidc_role=${PLUGIN_VAULT_OIDC_ROLE:-artifactapi-writer}
|
||||||
|
|
||||||
|
client_token=$(SA_JWT=$(cat "$sa") jq -nc --arg role "$role" '{role:$role,jwt:env.SA_JWT}' |
|
||||||
|
curl -sfS --max-time 30 -X POST --data @- "$vault/v1/auth/k8s/au/syd1/login" |
|
||||||
|
jq -er .auth.client_token) || { warn "vault login as $role failed"; return 0; }
|
||||||
|
token=$(printf 'X-Vault-Token: %s\n' "$client_token" |
|
||||||
|
curl -sfS --max-time 30 -H @- "$vault/v1/identity/oidc/token/$oidc_role" |
|
||||||
|
jq -er .data.token) || { warn "reading identity/oidc/token/$oidc_role failed"; return 0; }
|
||||||
|
|
||||||
|
PLUGIN_LOGINS=$(printf '%s' "${PLUGIN_LOGINS:-[]}" |
|
||||||
|
OIDC_JWT=$token jq -c --arg r "$ARTIFACTAPI_HOST" '. + [{registry:$r,username:"woodpecker",password:env.OIDC_JWT}]') ||
|
||||||
|
{ warn "PLUGIN_LOGINS is not valid JSON"; return 0; }
|
||||||
|
export PLUGIN_LOGINS
|
||||||
|
echo "artifactapi-login: logged in to $ARTIFACTAPI_HOST via vault role $role" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -z "${ARTIFACTAPI_LOGIN_NOEXEC:-}" ]; then
|
||||||
|
artifactapi_login
|
||||||
|
exec /usr/local/bin/dockerd-entrypoint.sh plugin-docker-buildx "$@"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -u
|
||||||
|
ARTIFACTAPI_LOGIN_NOEXEC=1 . "$(dirname "$0")/../artifactapi-login.sh"
|
||||||
|
|
||||||
|
SA_TOKEN_FILE=$(mktemp)
|
||||||
|
echo sa-jwt > "$SA_TOKEN_FILE"
|
||||||
|
REG=artifactapi.k8s.syd1.au.unkin.net
|
||||||
|
OK_LOGIN='{"registry":"'$REG'","username":"woodpecker","password":"oidc-jwt"}'
|
||||||
|
VAULT_FAIL=
|
||||||
|
|
||||||
|
no_secret_argv() { case "$*" in *sa-jwt*|*s.client*|*oidc-jwt*) echo "secret in argv: $*" >&2; return 99 ;; esac; }
|
||||||
|
jq() { no_secret_argv "$@" && command jq "$@"; }
|
||||||
|
|
||||||
|
curl() {
|
||||||
|
[ -z "$VAULT_FAIL" ] || return 22
|
||||||
|
no_secret_argv "$@" || return
|
||||||
|
case "$*" in
|
||||||
|
*auth/k8s/au/syd1/login*) grep -q '"role":"woodpecker_artifactapi_writer","jwt":"sa-jwt"' && echo '{"auth":{"client_token":"s.client"}}' ;;
|
||||||
|
*"-H @-"*identity/oidc/token/artifactapi-writer) grep -qx 'X-Vault-Token: s.client' && echo '{"data":{"token":"oidc-jwt"}}' ;;
|
||||||
|
*) return 22 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
check() {
|
||||||
|
name=$1 want=$2; shift 2
|
||||||
|
got=$(unset PLUGIN_REGISTRY PLUGIN_REPO PLUGIN_USERNAME PLUGIN_PASSWORD PLUGIN_LOGINS PLUGIN_DRY_RUN
|
||||||
|
for kv in "$@"; do export "$kv"; done
|
||||||
|
artifactapi_login 2>/dev/null; printf %s "${PLUGIN_LOGINS:-}")
|
||||||
|
if [ "$got" = "$want" ]; then echo "ok $name"; else echo "FAIL $name: got [$got] want [$want]"; fail=1; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check "registry setting logs in" "[$OK_LOGIN]" PLUGIN_REGISTRY=$REG
|
||||||
|
check "repo on artifactapi logs in" "[$OK_LOGIN]" PLUGIN_REPO=$REG/docker-internal/x
|
||||||
|
check "existing logins kept" '[{"registry":"r","username":"u","password":"p"},'"$OK_LOGIN]" PLUGIN_REGISTRY=$REG 'PLUGIN_LOGINS=[{"registry":"r","username":"u","password":"p"}]'
|
||||||
|
check "other registry skipped" "" PLUGIN_REPO=git.unkin.net/unkin/x
|
||||||
|
check "dry run skipped" "" PLUGIN_REGISTRY=$REG PLUGIN_DRY_RUN=true
|
||||||
|
check "explicit creds skipped" "" PLUGIN_REGISTRY=$REG PLUGIN_USERNAME=u PLUGIN_PASSWORD=p
|
||||||
|
check "creds for another registry still log in" "[$OK_LOGIN]" PLUGIN_REGISTRY=git.unkin.net PLUGIN_REPO=$REG/docker-internal/x PLUGIN_USERNAME=u PLUGIN_PASSWORD=p
|
||||||
|
check "explicit login skipped" '[{"registry":"'$REG'"}]' PLUGIN_REGISTRY=$REG 'PLUGIN_LOGINS=[{"registry":"'$REG'"}]'
|
||||||
|
check "missing sa token is anonymous" "" PLUGIN_REGISTRY=$REG SA_TOKEN_FILE=/nonexistent
|
||||||
|
check "vault failure is anonymous" "" PLUGIN_REGISTRY=$REG VAULT_FAIL=1
|
||||||
|
check "bad role is anonymous" "" PLUGIN_REGISTRY=$REG PLUGIN_VAULT_ROLE=other
|
||||||
|
exit $fail
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
IFS="|"; echo "$*"
|
||||||
Reference in New Issue
Block a user