Last active
April 7, 2025 08:36
-
-
Save anapsix/9e965d646b8c3549df6099d37bcdd3c0 to your computer and use it in GitHub Desktop.
K8s-OIDC-LOGIN - helper to simplify multi-cluster OIDC login and related configuration for kubectl. Can be used as kubectl plugin
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# | |
# K8s-OIDC-LOGIN helper to simplify configuration of OIDC authentication for kubectl | |
# | |
# Heavily influenced by oidckube project by @mrbobbytables | |
# https://github.com/mrbobbytables/oidckube | |
# | |
# Copyright (C) 2019 Anastas Dancha (aka @anapsix) | |
# This program is free software: you can redistribute it and/or modify | |
# it under the terms of the GNU General Public License as published by | |
# the Free Software Foundation, either version 3 of the License, or | |
# any later version. | |
# | |
# This program is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
# GNU General Public License for more details. | |
# | |
# You should have received a copy of the GNU General Public License | |
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |
set -e | |
set -u | |
set -o pipefail | |
: <<'EXAMPLE_CONFIG_YAML' | |
global: | |
oidc_server: keycloak.server.hostname.com | |
oidc_username: [email protected] | |
oidc_password: do-not-put-your-password-here | |
oidc_client_id: kubernetes | |
clusters: | |
cluster-name-1: | |
oidc_auth_realm: cluster-name-1-realm | |
oidc_client_secret: 33f12b49-faf9-498f-996a-c6cfe5d46d29 | |
cluster-name-2: | |
oidc_auth_realm: cluster-name-2-realm | |
oidc_client_secret: b1e512f9-02f0-442b-a1a0-b5c728c7254c | |
EXAMPLE_CONFIG_YAML | |
info() { | |
echo >&2 -e "[$(date)][\e[92mINFO\e[0m] $@" | |
} | |
debug() { | |
if [[ ${DEBUG:-0} -eq 1 ]]; then | |
echo >&2 -e "[$(date)][\e[95mDEBUG\e[0m] $@" | |
fi | |
} | |
error() { | |
local msg="$1" | |
local exit_code="${2:-1}" | |
echo >&2 -e "[$(date)][\e[91mERROR\e[0m] $1" | |
if [[ "${exit_code}" != "-" ]]; then | |
exit ${exit_code} | |
fi | |
} | |
usage() { | |
cat <<EOM | |
Usage: $0 completion [--debug] [--kubeconfig=kubectl-config-file] [--context=kubectl-context] | |
completion outputs bash completion script | |
-h | --help | --usage displays usage | |
--debug enables debug | |
--kubeconfig path to kubectl config; | |
defaults to value set by \$KUBECONFIG | |
curretly that's "${KUBECONFIG}" | |
--context kubectl context to work with, defaults to current context | |
--new-context kubectl context to be created / updated by the script, | |
with obtained OIDC tokens; | |
defaults to "<cluster name>@<username>" | |
Environent Variables: | |
K8S_OIDC_CONFIG_FILE path to config file | |
KUBECONFIG path to kubectl config file | |
EOM | |
} | |
output_completion() { | |
cat <<'EOM' | |
#!/usr/bin/env bash | |
KUBECONFIG=${KUBECONFIG:-~/.kube/config} | |
COMPREPLY=() | |
DEPS=( yq jq ) | |
check_dep() { | |
if ! which $1 2>&1 >/dev/null; then | |
echo >&2 "ERROR: dependency missing - \"${1}\"" | |
return 1 | |
fi | |
} | |
for dep in ${DEPS[*]}; do | |
check_dep $dep | |
done | |
if [ ! -r ${KUBECONFIG} ]; then | |
echo >&2 "ERROR: unable to read KUBECONFIG at \"${KUBECONFIG}\"" | |
return 1 | |
fi | |
_k8soidclogin_get_contexts() | |
{ | |
local contexts | |
if contexts=$(yq r -j ${KUBECONFIG} contexts[*].name | jq -r .[]); then | |
COMPREPLY+=( $(compgen -W "${contexts[*]}" -- "${_word_last}") ) | |
fi | |
} | |
_k8soidclogin_completion() | |
{ | |
local _word_index=$[${COMP_CWORD}-1] | |
local _word="${COMP_WORDS[$_word_index]}" | |
local _word_last="${COMP_WORDS[-1]}" | |
case $_word in | |
k8s-oidc-login.sh) | |
COMPREPLY+=( $(compgen -W "completion --debug --kubeconfig --context --new-context" -- "${_word_last}") ) | |
return | |
;; | |
--debug) | |
COMPREPLY=( $(compgen -W "completion --kubeconfig --context --new-context" -- "${_word_last}") ) | |
return | |
;; | |
completion) | |
return | |
;; | |
--context*) | |
_k8soidclogin_get_contexts | |
return | |
;; | |
-*) | |
return | |
;; | |
\>*) | |
return | |
;; | |
*) | |
COMPREPLY=( completion --debug --kubeconfig --context --new-context ) | |
return | |
;; | |
esac | |
} | |
if [[ $(type -t compopt) = "builtin" ]]; then | |
complete -o default -F _k8soidclogin_completion k8s-oidc-login.sh | |
else | |
complete -o default -o nospace -F _k8soidclogin_completion k8s-oidc-login.sh | |
fi | |
EOM | |
} | |
: ${K8S_OIDC_CONFIG_FILE:="$HOME/.kube/k8s-oidc-login.yaml"} | |
[[ -r "${K8S_OIDC_CONFIG_FILE}" ]] || \ | |
error "Unable to read config file at \"${K8S_OIDC_CONFIG_FILE}\", exiting.." | |
: ${KUBECONFIG:="${HOME}/.kube/config"} | |
## Get CLI arguments | |
while [[ $# -gt 0 ]]; do | |
debug "Checking arg: \"$1\"" | |
case "$1" in | |
-h|--help|--usage) | |
usage | |
exit 0 | |
;; | |
--debug) | |
DEBUG=1 | |
shift 1 | |
;; | |
completion) | |
output_completion | |
exit 0 | |
;; | |
--context|--context=*) | |
if [[ "${1:9:1}" == "=" ]]; then | |
export CURRENT_CONTEXT=${1##*=} | |
shift 1 | |
else | |
export CURRENT_CONTEXT="$2" | |
shift 2 | |
fi | |
debug "Current Context from Args: ${CURRENT_CONTEXT}" | |
;; | |
--new-context|--new-context=*) | |
if [[ "${1:13:1}" == "=" ]]; then | |
export NEW_CONTEXT=${1##*=} | |
shift 1 | |
else | |
export NEW_CONTEXT="$2" | |
shift 2 | |
fi | |
;; | |
--kubeconfig|--kubeconfig=*) | |
if [[ "${1:12:1}" == "=" ]]; then | |
export KUBECONFIG=${1##*=} | |
shift 1 | |
else | |
export KUBECONFIG="$2" | |
shift 2 | |
fi | |
debug "KUBECONFIG from Args: ${KUBECONFIG}" | |
;; | |
*) | |
error "Unexpected option \"$1\"" - | |
usage | |
exit 1 | |
;; | |
esac | |
done | |
DEPS=( uuidgen sed jq yq kubectl ) | |
FAILED_DEPS=( ) | |
check_dep() { | |
if ! which -s "${1}"; then | |
FAILED_DEPS+=( "$1" ) | |
fi | |
} | |
for dep in ${DEPS[@]}; do | |
check_dep "$dep" | |
done | |
if [ ${#FAILED_DEPS[@]} -ne 0 ]; then | |
error "Dependencies missing, exiting.." - | |
error "${FAILED_DEPS[@]}" | |
fi | |
read_config_value() { | |
yq r "${K8S_OIDC_CONFIG_FILE}" $1 | |
} | |
read_kubectl_config() { | |
yq r "${KUBECONFIG}" $1 | |
} | |
read_kubectl_config_json() { | |
yq r -j "${KUBECONFIG}" $1 | |
} | |
debug "KUBECONFIG: ${KUBECONFIG}" | |
if [[ ! -r "${KUBECONFIG}" ]]; then | |
error "KUBECONFIG not readable at \"$KUBECONFIG\", exiting.." | |
fi | |
if [[ "${CURRENT_CONTEXT:-unset}" == "unset" ]]; then | |
CURRENT_CONTEXT="$(read_kubectl_config current-context)" | |
fi | |
CONTEXT_CLUSTER="$( | |
read_kubectl_config_json contexts | \ | |
jq -r --arg context ${CURRENT_CONTEXT} '.[] | select(.name==$context) | .context.cluster' | |
)" | |
debug "Context Cluster: ${CONTEXT_CLUSTER}" | |
GLOBAL_OIDC_SERVER="$(read_config_value global.oidc_server)" | |
OIDC_SERVER="$(read_config_value clusters.${CONTEXT_CLUSTER}.oidc_server)" | |
( [[ "${OIDC_SERVER}" == "null" ]] || [[ -z "${OIDC_SERVER}" ]] ) && OIDC_SERVER="${GLOBAL_OIDC_SERVER}" | |
debug "OIDC Server: ${OIDC_SERVER}" | |
OIDC_AUTH_REALM=$(read_config_value clusters.${CONTEXT_CLUSTER}.oidc_auth_realm) | |
debug "OIDC Auth Realm: ${OIDC_AUTH_REALM}" | |
GLOBAL_OIDC_CLIENT_ID=$(read_config_value global.oidc_client_id) | |
OIDC_CLIENT_ID=$(read_config_value clusters.${CONTEXT_CLUSTER}.oidc_client_id) | |
( [[ "${OIDC_CLIENT_ID}" == "null" ]] || [[ -z "${OIDC_CLIENT_ID}" ]] ) && OIDC_CLIENT_ID="${GLOBAL_OIDC_CLIENT_ID}" | |
debug "OIDC Client ID: ${OIDC_CLIENT_ID}" | |
OIDC_CLIENT_SECRET="$(read_config_value clusters.${CONTEXT_CLUSTER}.oidc_client_secret)" | |
debug "OIDC Client Secret: ${OIDC_CLIENT_SECRET}" | |
GLOBAL_OIDC_USERNAME="$(read_config_value global.oidc_username)" | |
OIDC_USERNAME="$(read_config_value clusters.${CONTEXT_CLUSTER}.oidc_username)" | |
( [[ "${OIDC_USERNAME}" == "null" ]] || [[ -z "${OIDC_USERNAME}" ]] ) && OIDC_USERNAME="${GLOBAL_OIDC_USERNAME}" | |
debug "OIDC Username: ${OIDC_USERNAME}" | |
GLOBAL_OIDC_PASSWORD="$(read_config_value global.oidc_password)" | |
OIDC_PASSWORD="$(read_config_value clusters.${CONTEXT_CLUSTER}.oidc_password)" | |
( [[ "${OIDC_PASSWORD}" == "null" ]] || [[ -z "${OIDC_PASSWORD}" ]] ) && OIDC_PASSWORD="${GLOBAL_OIDC_PASSWORD}" | |
debug "OIDC Password: ${OIDC_PASSWORD}" | |
if [[ "${NEW_CONTEXT:-null}" == "null" ]]; then | |
NEW_CONTEXT="${CONTEXT_CLUSTER}@${OIDC_USERNAME}" | |
fi | |
debug "New Context: ${NEW_CONTEXT}" | |
config_reminder() { | |
error "Make sure \"${K8S_OIDC_CONFIG_FILE}\" contains per-cluster config, matching cluster names from \"${KUBECONFIG}\"" - | |
} | |
if [[ "${OIDC_SERVER:-null}" == "null" ]]; then | |
error "OIDC Server must be configured for cluster \"${CONTEXT_CLUSTER}\"" - | |
CONFIG_REMINDER=1 | |
fi | |
if [[ "${OIDC_AUTH_REALM:-null}" == "null" ]]; then | |
error "OIDC Realm must be configured for cluster \"${CONTEXT_CLUSTER}\"" - | |
CONFIG_REMINDER=1 | |
fi | |
if [[ "${OIDC_CLIENT_ID:-null}" == "null" ]]; then | |
error "OIDC Client ID must be configured for cluster \"${CONTEXT_CLUSTER}\"" - | |
CONFIG_REMINDER=1 | |
fi | |
if [[ "${OIDC_CLIENT_SECRET:-null}" == "null" ]]; then | |
error "OIDC Client secret must be configured for cluster \"${CONTEXT_CLUSTER}\"" - | |
CONFIG_REMINDER=1 | |
fi | |
if [[ ${CONFIG_REMINDER:-0} -ne 0 ]]; then | |
config_reminder | |
exit 1 | |
fi | |
# exit 5 | |
get_creds() { | |
echo "Please input your credentials for https://$OIDC_SERVER/auth/realms/$OIDC_AUTH_REALM" | |
if [[ "${OIDC_USERNAME:-null}" == "null" ]] ; then | |
read -rp "username / email: " OIDC_USERNAME | |
else | |
echo "username / email: ${OIDC_USERNAME}" | |
fi | |
if [[ "${OIDC_PASSWORD:-null}" = "null" ]]; then | |
read -rsp "password: " OIDC_PASSWORD | |
echo | |
fi | |
if [[ "${OIDC_TOTP:-null}" = "null" ]]; then | |
read -rp "TOTP [enter to skip]: " OIDC_TOTP | |
fi | |
} | |
get_token() { | |
local keycloak_token_url="https://${OIDC_SERVER}/auth/realms/${OIDC_AUTH_REALM}/protocol/openid-connect/token" | |
info "Requesting token from $keycloak_token_url" | |
TOKEN=$(curl -k -s "$keycloak_token_url" \ | |
-d grant_type=password \ | |
-d response_type=id_token \ | |
-d scope=openid \ | |
-d client_id="${OIDC_CLIENT_ID}" \ | |
-d client_secret="${OIDC_CLIENT_SECRET}" \ | |
-d username="${OIDC_USERNAME}" \ | |
-d password="${OIDC_PASSWORD}" \ | |
-d totp="${OIDC_TOTP}") | |
ERROR=$(echo "$TOKEN" | jq .error -r) | |
if [ "$ERROR" != "null" ];then | |
error "$TOKEN" - | |
return 1 | |
fi | |
} | |
set_creds() { | |
local id_token refresh_token | |
id_token=$(echo "$TOKEN" | jq .id_token -r) | |
refresh_token=$(echo "$TOKEN" | jq .refresh_token -r) | |
info "Adding user ${OIDC_USERNAME} to kube config" | |
kubectl config set-credentials "${OIDC_USERNAME}" \ | |
--auth-provider=oidc \ | |
--auth-provider-arg=idp-issuer-url="https://${OIDC_SERVER}/auth/realms/${OIDC_AUTH_REALM}" \ | |
--auth-provider-arg=client-id="${OIDC_CLIENT_ID}" \ | |
--auth-provider-arg=client-secret="${OIDC_CLIENT_SECRET}" \ | |
--auth-provider-arg=id-token="${id_token}" \ | |
--auth-provider-arg=refresh-token="${refresh_token}" | |
} | |
set_context() { | |
info "Adding / updating context ${NEW_CONTEXT}" | |
kubectl config set-context "${NEW_CONTEXT}" \ | |
--cluster="${CONTEXT_CLUSTER}" \ | |
--user="${OIDC_USERNAME}" | |
info "Example Usage: kubectl --kubeconfig=${KUBECONFIG} --context=${NEW_CONTEXT} get nodes,pods" | |
} | |
main() { | |
get_creds | |
get_token | |
set_creds | |
set_context | |
} | |
main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment