From 3497bf82b770c08e0c5854371da574cb941a95ad Mon Sep 17 00:00:00 2001 From: Jake Nabasny Date: Sat, 9 Nov 2024 18:52:28 -0600 Subject: [PATCH 1/2] add identity-platform bundle --- common/ch_channel_map/any_series | 4 ++ common/charm_lists | 11 +++ identity-platform/authentik-values.yaml | 21 ++++++ identity-platform/common | 1 + identity-platform/configure | 63 ++++++++++++++++ identity-platform/generate-bundle.sh | 1 + identity-platform/iam.yaml.template | 72 +++++++++++++++++++ identity-platform/module_defaults | 9 +++ identity-platform/overlays | 1 + identity-platform/pipeline/00setup | 22 ++++++ .../pipeline/01import-config-defaults | 2 + identity-platform/pipeline/02configure | 33 +++++++++ identity-platform/pipeline/03build | 5 ++ overlays/kubernetes/k8s-iam-grafana.yaml | 11 +++ overlays/kubernetes/k8s-iam-oidc.yaml | 9 +++ 15 files changed, 265 insertions(+) create mode 100644 identity-platform/authentik-values.yaml create mode 120000 identity-platform/common create mode 100755 identity-platform/configure create mode 120000 identity-platform/generate-bundle.sh create mode 100644 identity-platform/iam.yaml.template create mode 100644 identity-platform/module_defaults create mode 120000 identity-platform/overlays create mode 100644 identity-platform/pipeline/00setup create mode 100644 identity-platform/pipeline/01import-config-defaults create mode 100644 identity-platform/pipeline/02configure create mode 100644 identity-platform/pipeline/03build create mode 100644 overlays/kubernetes/k8s-iam-grafana.yaml create mode 100644 overlays/kubernetes/k8s-iam-oidc.yaml diff --git a/common/ch_channel_map/any_series b/common/ch_channel_map/any_series index dd672f57..b2595b76 100644 --- a/common/ch_channel_map/any_series +++ b/common/ch_channel_map/any_series @@ -12,5 +12,9 @@ for c in ${CEPH_CHARMS[@]}; do CHARM_CHANNEL[$c]=$ceph_release/edge done +for c in ${IAM_CHARMS[@]}; do + CHARM_CHANNEL[$c]=latest/edge +done + CHARM_CHANNEL[pacemaker-remote]=${series}/edge CHARM_CHANNEL[microk8s]=1.28/stable diff --git a/common/charm_lists b/common/charm_lists index aa19f83f..97fd13d2 100644 --- a/common/charm_lists +++ b/common/charm_lists @@ -88,3 +88,14 @@ prometheus ro zookeeper ) + +declare -a IAM_CHARMS=( +hydra +identity-platform-login-ui-operator +kratos +kratos-external-idp-integrator +oathkeeper +postgresql-k8s +self-signed-certificates +traefik-k8s +) diff --git a/identity-platform/authentik-values.yaml b/identity-platform/authentik-values.yaml new file mode 100644 index 00000000..d39baf87 --- /dev/null +++ b/identity-platform/authentik-values.yaml @@ -0,0 +1,21 @@ +authentik: + secret_key: "my-secure-secret-key" + error_reporting: + enabled: false + postgresql: + password: "my-secure-psql-password" + bootstrap_token: "my-secure-bootstrap-token" + bootstrap_password: "Passw0rd" +server: + ingress: + ingressClassName: nginx + enabled: false + hosts: + - authentik.secloud +postgresql: + enabled: true + auth: + password: "my-secure-psql-password" +redis: + enabled: true + diff --git a/identity-platform/common b/identity-platform/common new file mode 120000 index 00000000..60d3b0a6 --- /dev/null +++ b/identity-platform/common @@ -0,0 +1 @@ +../common \ No newline at end of file diff --git a/identity-platform/configure b/identity-platform/configure new file mode 100755 index 00000000..9e872002 --- /dev/null +++ b/identity-platform/configure @@ -0,0 +1,63 @@ +#!/bin/bash -x + +# Check for kratos-external-idp-integrator +if [ "$(juju status --format json| jq -r '.applications["kratos-external-idp-integrator"].units|to_entries[]|select(.value["leader"])|.key' 2> /dev/null)" == "" ]; then + echo 'ERROR: Cannot configure OIDC without kratos-external-idp-integrator!' + exit 1 +fi + +# Install Helm +if ! snap list | grep -q helm; then + sudo snap install helm --classic +fi + +# Install authentik +kubectl get ns authentik &> /dev/null || kubectl create ns authentik +https_proxy=http://squid.internal:3128 helm repo add authentik https://charts.goauthentik.io +https_proxy=http://squid.internal:3128 helm repo update +helm install authentik authentik/authentik -f ./authentik-values.yaml -n authentik || exit 1 + +# Prepare port for API calls and wait +kubectl patch svc -n authentik authentik-server -p '{"spec": {"type": "NodePort"}}' || exit 1 +AUTH_PORT=$(kubectl get svc -n authentik authentik-server -o jsonpath='{.spec.ports[].nodePort}') +AUTH_IP=$(kubectl get po -n authentik -o json | jq -r '.items[] | select(.metadata.name | test("authentik-server-")) | .status.hostIP') + +timeout=0 +echo 'Waiting for Authentik to start...' +until curl -s -X GET -H "accept: application/json" -H "Authorization: Bearer my-secure-bootstrap-token" "http://${AUTH_IP}:${AUTH_PORT}/api/v3/admin/system/" ; do + if [[ $timeout == 600 ]]; then + echo 'ERROR: Authentik failed to start.' + exit 1 + fi + sleep 1 + ((timeout++)) +done + +# Configure OIDC +## get auth flow id +FLOW=$(curl -s -X GET -H "accept: application/json" -H "Authorization: Bearer my-secure-bootstrap-token" "http://${AUTH_IP}:${AUTH_PORT}/api/v3/flows/instances/?search=default-authentication-flow" | jq -r '.results[0].pk') + +## create provider +curl -X POST "http://${AUTH_IP}:${AUTH_PORT}/api/v3/providers/oauth2/" -H "Authorization: Bearer my-secure-bootstrap-token" -H "accept: application/json" -H "content-type: application/json" -d "{\"name\":\"oidc-provider\",\"authentication_flow\":\"$FLOW\",\"authorization_flow\":\"$FLOW\",\"invalidation_flow\":\"$FLOW\",\"client_type\":\"confidential\",\"client_id\":\"canonical-support\",\"client_secret\":\"my-secure-oidc-secret\",\"access_code_validity\":\"hours=3\",\"access_token_validity\":\"hours=3\",\"refresh_token_validity\":\"hours=3\",\"include_claims_in_id_token\":false,\"redirect_uris\":\"*\",\"sub_mode\":\"hashed_user_id\",\"issuer_mode\":\"per_provider\"}" + +## create app +curl -H "Authorization: Bearer my-secure-bootstrap-token" -X POST "http://${AUTH_IP}:${AUTH_PORT}/api/v3/core/applications/" -H "accept: application/json" -H "content-type: application/json" -d '{"name":"canonical-support","slug":"canonical-support","provider":1,"backchannel_providers":[1],"policy_engine_mode":"all"}' + +# Configure kratos +juju config kratos-external-idp-integrator provider=generic +juju config kratos-external-idp-integrator client_id=canonical-support +juju config kratos-external-idp-integrator client_secret=my-secure-oidc-secret +juju config kratos-external-idp-integrator issuer_url=http://"${AUTH_IP}:${AUTH_PORT}"/application/o/canonical-support/ + +set +x +echo " +Configuration is complete! You can test a login with the following credentials: + +Authentik Dashboard: http://${AUTH_IP}:${AUTH_PORT} +OIDC User: akadmin +Password: Passw0rd" + +grafana_url="$(juju run grafana/0 get-admin-password 2> /dev/null | grep url | sed -e 's/url: //')" +if [[ -n $grafana_url ]]; then + echo "Grafana Dashboard: $grafana_url" +fi diff --git a/identity-platform/generate-bundle.sh b/identity-platform/generate-bundle.sh new file mode 120000 index 00000000..394558ee --- /dev/null +++ b/identity-platform/generate-bundle.sh @@ -0,0 +1 @@ +common/generate-bundle.sh \ No newline at end of file diff --git a/identity-platform/iam.yaml.template b/identity-platform/iam.yaml.template new file mode 100644 index 00000000..8920a232 --- /dev/null +++ b/identity-platform/iam.yaml.template @@ -0,0 +1,72 @@ +--- +bundle: kubernetes +name: identity-platform +website: https://github.com/canonical/iam-bundle +issues: https://github.com/canonical/iam-bundle/issues +applications: + hydra: + charm: __CHARM_STORE____CHARM_CS_NS____CHARM_CH_PREFIX__hydra + scale: 1 + series: jammy + trust: true + identity-platform-login-ui-operator: + charm: __CHARM_STORE____CHARM_CS_NS____CHARM_CH_PREFIX__identity-platform-login-ui-operator + scale: 1 + series: jammy + trust: true + kratos: + charm: __CHARM_STORE____CHARM_CS_NS____CHARM_CH_PREFIX__kratos + scale: 1 + series: jammy + options: + enforce_mfa: false + trust: true + oathkeeper: + charm: __CHARM_STORE____CHARM_CS_NS____CHARM_CH_PREFIX__oathkeeper + scale: 1 + series: jammy + trust: true + postgresql-k8s: + charm: __CHARM_STORE____CHARM_CS_NS____CHARM_CH_PREFIX__postgresql-k8s + scale: 1 + series: jammy + options: + plugin_btree_gin_enable: true + plugin_pg_trgm_enable: true + storage: + pgdata: kubernetes,1,1024M + trust: true + self-signed-certificates: + charm: __CHARM_STORE____CHARM_CS_NS____CHARM_CH_PREFIX__self-signed-certificates + scale: 1 + traefik-admin: + charm: __CHARM_STORE____CHARM_CS_NS____CHARM_CH_PREFIX__traefik-k8s + scale: 1 + series: focal + storage: + configurations: kubernetes,1,1024M + trust: true + traefik-public: + charm: __CHARM_STORE____CHARM_CS_NS____CHARM_CH_PREFIX__traefik-k8s + scale: 1 + series: focal + options: + enable_experimental_forward_auth: true + storage: + configurations: kubernetes,1,1024M + trust: true +relations: + - [hydra:pg-database, postgresql-k8s:database] + - [kratos:pg-database, postgresql-k8s:database] + - [kratos:hydra-endpoint-info, hydra:hydra-endpoint-info] + - [hydra:admin-ingress, traefik-admin:ingress] + - [hydra:public-ingress, traefik-public:ingress] + - [kratos:admin-ingress, traefik-admin:ingress] + - [kratos:public-ingress, traefik-public:ingress] + - [identity-platform-login-ui-operator:ingress, traefik-public:ingress] + - [identity-platform-login-ui-operator:hydra-endpoint-info, hydra:hydra-endpoint-info] + - [identity-platform-login-ui-operator:ui-endpoint-info, hydra:ui-endpoint-info] + - [identity-platform-login-ui-operator:ui-endpoint-info, kratos:ui-endpoint-info] + - [identity-platform-login-ui-operator:kratos-info, kratos:kratos-info] + - [traefik-admin:certificates, self-signed-certificates:certificates] + - [traefik-public:certificates, self-signed-certificates:certificates] diff --git a/identity-platform/module_defaults b/identity-platform/module_defaults new file mode 100644 index 00000000..21ffaa2e --- /dev/null +++ b/identity-platform/module_defaults @@ -0,0 +1,9 @@ +# This file must contain defaults for all variables used in bundles/overlays. +# They are used to render to final product in the event they are not provided +# elsewhere. It is inserted into the global context at the start of the +# pipeline. +# +# You can check that none are missing by running lint/check_var_defaults.sh +# +JUJU_DEPLOY_OPTS=" --trust" +CHARM_CHANNEL[postgresql-k8s]=14/stable diff --git a/identity-platform/overlays b/identity-platform/overlays new file mode 120000 index 00000000..0d44a21c --- /dev/null +++ b/identity-platform/overlays @@ -0,0 +1 @@ +../overlays \ No newline at end of file diff --git a/identity-platform/pipeline/00setup b/identity-platform/pipeline/00setup new file mode 100644 index 00000000..6b183cc5 --- /dev/null +++ b/identity-platform/pipeline/00setup @@ -0,0 +1,22 @@ +#!/bin/bash + +# Globals +export MOD_NAME=identity-platform +export MOD_BASE_TEMPLATE=iam.yaml.template +export MOD_SSL_STATE_DIR=${MOD_NAME} +[ -n "${MASTER_OPTS[BUNDLE_NAME]}" ] && \ + MOD_SSL_STATE_DIR="${MOD_SSL_STATE_DIR}-${MASTER_OPTS[BUNDLE_NAME]}" + +# opts that 02configure does not recognise that get passed to the generator +export -a MOD_PASSTHROUGH_OPTS=() + +# Collection of messages to display at the end +export -A MOD_MSGS=() +# Use order 0 to ensure this is first displayed +MOD_MSGS[0_common.0]="Ensure a LoadBalancer (e.g. MetalLB or Cilium) is enabled on k8s" +MOD_MSGS[0_common.2]="Configure a local user: juju run kratos/0 create-admin-account email=admin@secloud.local password=Passw0rd username=admin" + +# Array list of overlays to use with this deployment. +export -a MOD_OVERLAYS=() + +export -A MOD_PARAMS=() diff --git a/identity-platform/pipeline/01import-config-defaults b/identity-platform/pipeline/01import-config-defaults new file mode 100644 index 00000000..8848bc10 --- /dev/null +++ b/identity-platform/pipeline/01import-config-defaults @@ -0,0 +1,2 @@ +# Current module imports +. $MOD_DIR/module_defaults diff --git a/identity-platform/pipeline/02configure b/identity-platform/pipeline/02configure new file mode 100644 index 00000000..58c94fb1 --- /dev/null +++ b/identity-platform/pipeline/02configure @@ -0,0 +1,33 @@ +#!/bin/bash +# Global variables are first defined in 00setup and module +# dependencies are defined in 01import-config-defaults +# +# All overlay/bundle variables (MOD_PARAMS) defaults must go into +# the /module_defaults file. + +cloud="$(get_cloud_type)" +if [[ "$cloud" != "k8s" ]]; then + echo "ERROR: Must switch to a Kubernetes model first." + exit 1 +fi + +while (($# > 0)) +do + case $1 in + --oidc) + MOD_OVERLAYS+=( "kubernetes/k8s-iam-oidc.yaml" ) + MOD_MSGS[0_common.1]="Setup OIDC: ./configure" + ;; + --grafana) + MOD_OVERLAYS+=( "kubernetes/k8s-iam-grafana.yaml" ) + MOD_MSGS[grafana.0]="Get Grafana URL: juju run grafana/leader get-admin-password" + ;; + *) + echo "ERROR: invalid input '$1'" + _usage + exit 1 + ;; + esac + shift +done + diff --git a/identity-platform/pipeline/03build b/identity-platform/pipeline/03build new file mode 100644 index 00000000..62dd78f9 --- /dev/null +++ b/identity-platform/pipeline/03build @@ -0,0 +1,5 @@ +#!/bin/bash +. $MOD_DIR/common/generate_bundle_base + +print_msgs + diff --git a/overlays/kubernetes/k8s-iam-grafana.yaml b/overlays/kubernetes/k8s-iam-grafana.yaml new file mode 100644 index 00000000..63f6e387 --- /dev/null +++ b/overlays/kubernetes/k8s-iam-grafana.yaml @@ -0,0 +1,11 @@ +applications: + grafana: + charm: __CHARM_STORE____CHARM_CS_NS____CHARM_CH_PREFIX__grafana-k8s + scale: 1 + series: focal + storage: + database: kubernetes,1,1024M +relations: + - [grafana:ingress, traefik-public:traefik-route] + - [grafana:oauth, hydra:oauth] + - [grafana:receive-ca-cert, self-signed-certificates:send-ca-cert] diff --git a/overlays/kubernetes/k8s-iam-oidc.yaml b/overlays/kubernetes/k8s-iam-oidc.yaml new file mode 100644 index 00000000..445c7710 --- /dev/null +++ b/overlays/kubernetes/k8s-iam-oidc.yaml @@ -0,0 +1,9 @@ +applications: + kratos-external-idp-integrator: + charm: __CHARM_STORE____CHARM_CS_NS____CHARM_CH_PREFIX__kratos-external-idp-integrator + scale: 1 + series: jammy + options: + provider: generic +relations: + - [kratos-external-idp-integrator:kratos-external-idp, kratos:kratos-external-idp] From 03fc6735e98e94379ac0bd3453052da43dec5cca Mon Sep 17 00:00:00 2001 From: Jake Nabasny Date: Tue, 26 Nov 2024 15:40:24 -0600 Subject: [PATCH 2/2] Fixes to Authentik deploy - set version due to API changes in later version - improve API calls --- identity-platform/configure | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/identity-platform/configure b/identity-platform/configure index 9e872002..53c2d7dc 100755 --- a/identity-platform/configure +++ b/identity-platform/configure @@ -1,4 +1,10 @@ -#!/bin/bash -x +#!/bin/bash + +# Reset +if [[ "$1" == "reset" ]]; then + helm uninstall authentik -n authentik + kubectl delete pvc -n authentik data-authentik-postgresql-0 redis-data-authentik-redis-master-0 +fi # Check for kratos-external-idp-integrator if [ "$(juju status --format json| jq -r '.applications["kratos-external-idp-integrator"].units|to_entries[]|select(.value["leader"])|.key' 2> /dev/null)" == "" ]; then @@ -15,16 +21,13 @@ fi kubectl get ns authentik &> /dev/null || kubectl create ns authentik https_proxy=http://squid.internal:3128 helm repo add authentik https://charts.goauthentik.io https_proxy=http://squid.internal:3128 helm repo update -helm install authentik authentik/authentik -f ./authentik-values.yaml -n authentik || exit 1 - -# Prepare port for API calls and wait -kubectl patch svc -n authentik authentik-server -p '{"spec": {"type": "NodePort"}}' || exit 1 -AUTH_PORT=$(kubectl get svc -n authentik authentik-server -o jsonpath='{.spec.ports[].nodePort}') -AUTH_IP=$(kubectl get po -n authentik -o json | jq -r '.items[] | select(.metadata.name | test("authentik-server-")) | .status.hostIP') +helm install authentik authentik/authentik -f ./authentik-values.yaml -n authentik --version 2024.10.1 timeout=0 echo 'Waiting for Authentik to start...' -until curl -s -X GET -H "accept: application/json" -H "Authorization: Bearer my-secure-bootstrap-token" "http://${AUTH_IP}:${AUTH_PORT}/api/v3/admin/system/" ; do +up=0 +while [[ "$up" != 1 ]]; do + up="$(kubectl get deploy -n authentik authentik-server -o json | jq '.status.readyReplicas')" if [[ $timeout == 600 ]]; then echo 'ERROR: Authentik failed to start.' exit 1 @@ -33,15 +36,24 @@ until curl -s -X GET -H "accept: application/json" -H "Authorization: Bearer my- ((timeout++)) done +# Prepare port for API calls and wait +kubectl patch svc -n authentik authentik-server -p '{"spec": {"type": "NodePort"}}' || exit 1 +AUTH_PORT=$(kubectl get svc -n authentik authentik-server -o jsonpath='{.spec.ports[].nodePort}') +AUTH_IP=$(kubectl get po -n authentik -o json | jq -r '.items[] | select(.metadata.name | test("authentik-server-")) | .status.hostIP') + # Configure OIDC -## get auth flow id -FLOW=$(curl -s -X GET -H "accept: application/json" -H "Authorization: Bearer my-secure-bootstrap-token" "http://${AUTH_IP}:${AUTH_PORT}/api/v3/flows/instances/?search=default-authentication-flow" | jq -r '.results[0].pk') +## get default values +until [ -n "$AUTH_FLOW" ]; do AUTH_FLOW=$(curl -s -X GET -H "accept: application/json" -H "Authorization: Bearer my-secure-bootstrap-token" "http://${AUTH_IP}:${AUTH_PORT}/api/v3/flows/instances/?search=default-authentication-flow" | jq -r '.results[0].pk'); done +until [ -n "$AUTHZ_FLOW" ]; do AUTHZ_FLOW=$(curl -s -X GET -H "accept: application/json" -H "Authorization: Bearer my-secure-bootstrap-token" "http://${AUTH_IP}:${AUTH_PORT}/api/v3/flows/instances/?search=default-provider-authorization-implicit-consent" | jq -r '.results[0].pk'); done +until [ -n "$INVALID_FLOW" ]; do INVALID_FLOW=$(curl -s -X GET -H "accept: application/json" -H "Authorization: Bearer my-secure-bootstrap-token" "http://${AUTH_IP}:${AUTH_PORT}/api/v3/flows/instances/?search=default-invalidation-flow" | jq -r '.results[0].pk'); done +until [ -n "$SIGN_KEY" ]; do SIGN_KEY=$(curl -s -X GET -H "accept: application/json" -H "Authorization: Bearer my-secure-bootstrap-token" "http://${AUTH_IP}:${AUTH_PORT}/api/v3/crypto/certificatekeypairs/" | jq -r '.results[0].pk'); done +until [ -n "$SCOPE" ]; do SCOPE=$(curl -s -X GET -H "accept: application/json" -H "Authorization: Bearer my-secure-bootstrap-token" "http://${AUTH_IP}:${AUTH_PORT}/api/v3/propertymappings/provider/scope/?search=email" | jq -r '.results[0].pk'); done ## create provider -curl -X POST "http://${AUTH_IP}:${AUTH_PORT}/api/v3/providers/oauth2/" -H "Authorization: Bearer my-secure-bootstrap-token" -H "accept: application/json" -H "content-type: application/json" -d "{\"name\":\"oidc-provider\",\"authentication_flow\":\"$FLOW\",\"authorization_flow\":\"$FLOW\",\"invalidation_flow\":\"$FLOW\",\"client_type\":\"confidential\",\"client_id\":\"canonical-support\",\"client_secret\":\"my-secure-oidc-secret\",\"access_code_validity\":\"hours=3\",\"access_token_validity\":\"hours=3\",\"refresh_token_validity\":\"hours=3\",\"include_claims_in_id_token\":false,\"redirect_uris\":\"*\",\"sub_mode\":\"hashed_user_id\",\"issuer_mode\":\"per_provider\"}" +curl -X POST "http://${AUTH_IP}:${AUTH_PORT}/api/v3/providers/oauth2/" -H "Authorization: Bearer my-secure-bootstrap-token" -H "accept: application/json" -H "content-type: application/json" -d "{\"name\":\"oidc-provider\",\"authentication_flow\":\"$AUTH_FLOW\",\"authorization_flow\":\"$AUTHZ_FLOW\",\"invalidation_flow\":\"$INVALID_FLOW\",\"client_type\":\"confidential\",\"client_id\":\"canonical-support\",\"client_secret\":\"my-secure-oidc-secret\",\"access_code_validity\":\"hours=3\",\"access_token_validity\":\"hours=3\",\"refresh_token_validity\":\"hours=3\",\"include_claims_in_id_token\":true,\"redirect_uris\":\"*\",\"sub_mode\":\"hashed_user_id\",\"issuer_mode\":\"per_provider\",\"signing_key\":\"$SIGN_KEY\",\"property_mappings\":[\"$SCOPE\"]}" ## create app -curl -H "Authorization: Bearer my-secure-bootstrap-token" -X POST "http://${AUTH_IP}:${AUTH_PORT}/api/v3/core/applications/" -H "accept: application/json" -H "content-type: application/json" -d '{"name":"canonical-support","slug":"canonical-support","provider":1,"backchannel_providers":[1],"policy_engine_mode":"all"}' +curl -H "Authorization: Bearer my-secure-bootstrap-token" -X POST "http://${AUTH_IP}:${AUTH_PORT}/api/v3/core/applications/" -H "accept: application/json" -H "content-type: application/json" -d '{"name":"canonical-support","slug":"canonical-support","provider":1,"policy_engine_mode":"all"}' # Configure kratos juju config kratos-external-idp-integrator provider=generic @@ -49,7 +61,6 @@ juju config kratos-external-idp-integrator client_id=canonical-support juju config kratos-external-idp-integrator client_secret=my-secure-oidc-secret juju config kratos-external-idp-integrator issuer_url=http://"${AUTH_IP}:${AUTH_PORT}"/application/o/canonical-support/ -set +x echo " Configuration is complete! You can test a login with the following credentials: