Skip to content

Instantly share code, notes, and snippets.

@anapsix
Last active January 9, 2023 13:55
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
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
#!/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: user@domain.com
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