Merge pull request 'Log in to artifactapi with a Vault identity OIDC token' (#2) from benvin/artifactapi-vault-login into main
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
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
|
||||
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)
|
||||
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
|
||||
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
|
||||
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