diff --git a/Untitled-1.json b/Untitled-1.json new file mode 100644 index 00000000..5c3d0c5a --- /dev/null +++ b/Untitled-1.json @@ -0,0 +1,18 @@ +{ + "name": "mini-lab", + "description": "mini-lab IdP", + "type": "Traditional", + "oidcClientMetadata": { + "redirectUris": [ + { + "postLogoutRedirectUris": [ + "http://v2.api.172.17.0.1.nip.io:8080/auth/oidc/callback" + ] + } + ], + "postLogoutRedirectUris": [ + "http://v2.api.172.17.0.1.nip.io:8080/auth/oidc/callback" + ] + }, + "isThirdParty": true +} diff --git a/deploy_control_plane.yaml b/deploy_control_plane.yaml index 2b0312cc..9aad70d7 100644 --- a/deploy_control_plane.yaml +++ b/deploy_control_plane.yaml @@ -27,6 +27,8 @@ tags: valkey - name: auth-dex tags: auth + - name: logto + tags: auth - name: metal-roles/control-plane/roles/metal tags: metal diff --git a/files/certs/logto-admin/server.json b/files/certs/logto-admin/server.json new file mode 100644 index 00000000..236fc9aa --- /dev/null +++ b/files/certs/logto-admin/server.json @@ -0,0 +1,24 @@ +{ + "CN": "logto", + "hosts": [ + "localhost", + "logto", + "logto.metal-control-plane.svc", + "logto.metal-control-plane.svc.cluster.local", + "logto-admin.172.17.0.1.nip.io", + "logto.172.17.0.1.nip.io" + ], + "key": { + "algo": "rsa", + "size": 4096 + }, + "names": [ + { + "C": "DE", + "L": "Muenchen", + "O": "metal-stack", + "OU": "DevOps", + "ST": "Bayern" + } + ] +} diff --git a/inventories/group_vars/control-plane/logto.yaml b/inventories/group_vars/control-plane/logto.yaml new file mode 100644 index 00000000..89957f40 --- /dev/null +++ b/inventories/group_vars/control-plane/logto.yaml @@ -0,0 +1,9 @@ +--- +auth_logto_admin_ingress_dns: "logto-admin.{{ metal_control_plane_ingress_dns }}" +auth_logto_ingress_dns: "logto.{{ metal_control_plane_ingress_dns }}" +auth_logto_issuer_url: http://logto.{{ metal_control_plane_ingress_dns }} + + +logto_admin_certs_server_key: "{{ lookup('file', 'certs/logto-admin/server-key.pem') }}" +logto_admin_certs_server_cert: "{{ lookup('file', 'certs/logto-admin/server.pem') }}" +logto_admin_certs_ca: "{{ lookup('file', 'certs/ca.pem') }}" diff --git a/logto-admin.sh b/logto-admin.sh new file mode 100644 index 00000000..902506a5 --- /dev/null +++ b/logto-admin.sh @@ -0,0 +1,93 @@ +#!/bin/bash +set -euo pipefail + +# =============================== +# Konfiguration +# =============================== +LOGTO_DB_CONTAINER="logto-postgres-1" +LOGTO_DB_USER="postgres" +LOGTO_DB_NAME="logto" +LOGTO_API_URL="http://localhost:3002" +CLIENT_ID="m-admin" +RESOURCE="https://admin.logto.app/api" +SCOPE="all" + +APP_NAME="mini-lab" +APP_DESCRIPTION="mini-lab IdP" +REDIRECT_URI="http://v2.api.172.17.0.1.nip.io:8080/auth/oidc/callback" + +# =============================== +# 1. Hole m-admin secret aus der Datenbank +# =============================== +echo "🔑 Hole Client Secret für ${CLIENT_ID}..." +CLIENT_SECRET=$(docker exec -it "$LOGTO_DB_CONTAINER" sh -c \ + "psql -U $LOGTO_DB_USER -d $LOGTO_DB_NAME -t -A -c \"SELECT secret FROM applications WHERE id = '$CLIENT_ID';\"" \ + | tr -d '\r') + +if [[ -z "$CLIENT_SECRET" ]]; then + echo "❌ Konnte Client Secret nicht finden. Bitte prüfe, ob die Datenbank läuft und m-admin existiert." + exit 1 +fi + +echo "✅ Secret gefunden: ${CLIENT_SECRET}" + +# =============================== +# 2. Hole Access Token +# =============================== +echo "🔐 Fordere Access Token an..." +ACCESS_TOKEN=$(curl -s --location \ + --request POST "$LOGTO_API_URL/oidc/token" \ + --header "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "grant_type=client_credentials" \ + --data-urlencode "client_id=$CLIENT_ID" \ + --data-urlencode "client_secret=$CLIENT_SECRET" \ + --data-urlencode "resource=$RESOURCE" \ + --data-urlencode "scope=$SCOPE" \ + | jq -r '.access_token') + +if [[ "$ACCESS_TOKEN" == "null" || -z "$ACCESS_TOKEN" ]]; then + echo "❌ Fehler beim Abrufen des Access Tokens." + exit 1 +fi + +echo "✅ Access Token erfolgreich erhalten." + +# =============================== +# 3. (Optional) Liste bestehende Third-Party-Apps +# =============================== +echo "📜 Bestehende Third-Party-Anwendungen:" +curl -s --location \ + --request GET "$LOGTO_API_URL/api/applications?isThirdParty=true" \ + --header "Authorization: Bearer $ACCESS_TOKEN" \ + | jq '.[] | {id, name, description}' + +# =============================== +# 4. Erstelle mini-lab Anwendung +# =============================== +echo "🚀 Erstelle neue Anwendung: $APP_NAME ..." +CREATE_RESPONSE=$(curl -s -w "\n%{http_code}" --location \ + --request POST "$LOGTO_API_URL/api/applications" \ + --header "Authorization: Bearer $ACCESS_TOKEN" \ + --header "Content-Type: application/json" \ + --data "{ + \"name\": \"$APP_NAME\", + \"description\": \"$APP_DESCRIPTION\", + \"type\": \"Traditional\", + \"oidcClientMetadata\": { + \"redirectUris\": [\"$REDIRECT_URI\"], + \"postLogoutRedirectUris\": [\"$REDIRECT_URI\"] + }, + \"isThirdParty\": true + }") + +HTTP_CODE=$(echo "$CREATE_RESPONSE" | tail -n1) +BODY=$(echo "$CREATE_RESPONSE" | sed '$d') + +if [[ "$HTTP_CODE" != "200" && "$HTTP_CODE" != "201" ]]; then + echo "❌ Fehler beim Erstellen der Anwendung (HTTP $HTTP_CODE):" + echo "$BODY" | jq . + exit 1 +fi + +echo "✅ Anwendung erfolgreich erstellt:" +echo "$BODY" | jq . diff --git a/logto-user.sh b/logto-user.sh new file mode 100644 index 00000000..ccc4babc --- /dev/null +++ b/logto-user.sh @@ -0,0 +1,161 @@ +#!/bin/bash +set -euo pipefail + +# =============================== +# Konfiguration +# =============================== +LOGTO_DB_CONTAINER="logto-postgres-1" +LOGTO_DB_USER="postgres" +LOGTO_DB_NAME="logto" +LOGTO_API_URL="http://localhost:3002" +CLIENT_ID="m-admin" +RESOURCE="https://admin.logto.app/api" +SCOPE="all" + +APP_NAME="mini-lab" +APP_DESCRIPTION="mini-lab IdP" +REDIRECT_URI="http://v2.api.172.17.0.1.nip.io:8080/auth/oidc/callback" + +ADMIN_NAME="admin" +ADMIN_PW="password1234" + +# =============================== +# 1. Hole m-admin secret aus der Datenbank +# =============================== +echo "🔑 Hole Client Secret für ${CLIENT_ID}..." +CLIENT_SECRET=$(docker exec -it "$LOGTO_DB_CONTAINER" sh -c \ + "psql -U $LOGTO_DB_USER -d $LOGTO_DB_NAME -t -A -c \"SELECT secret FROM applications WHERE id = '$CLIENT_ID';\"" \ + | tr -d '\r') +if [[ -z "$CLIENT_SECRET" ]]; then + echo "❌ Konnte Client Secret nicht finden. Bitte prüfe, ob die Datenbank läuft und m-admin existiert." + exit 1 +fi +echo "✅ Secret gefunden: ${CLIENT_SECRET}" + +# =============================== +# 2. Hole Access Token +# =============================== +echo "🔐 Fordere Access Token an..." +ACCESS_TOKEN=$(curl -s --location \ + --request POST "$LOGTO_API_URL/oidc/token" \ + --header "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "grant_type=client_credentials" \ + --data-urlencode "client_id=$CLIENT_ID" \ + --data-urlencode "client_secret=$CLIENT_SECRET" \ + --data-urlencode "resource=$RESOURCE" \ + --data-urlencode "scope=$SCOPE" \ + | jq -r '.access_token') +if [[ "$ACCESS_TOKEN" == "null" || -z "$ACCESS_TOKEN" ]]; then + echo "❌ Fehler beim Abrufen des Access Tokens." + exit 1 +fi +echo "✅ Access Token erfolgreich erhalten." + +# Create admin-user +echo "👤 Erstelle Admin-Benutzer..." +CREATE_RESPONSE=$(curl -s -w "\n%{http_code}" --location \ + --request POST "$LOGTO_API_URL/api/users" \ + --header "Authorization: Bearer $ACCESS_TOKEN" \ + --header "Content-Type: application/json" \ + --data "{ + \"username\": \"$ADMIN_NAME\", + \"password\": \"$ADMIN_PW\" + }") +HTTP_CODE=$(echo "$CREATE_RESPONSE" | tail -n1) +BODY=$(echo "$CREATE_RESPONSE" | sed '$d') +if [[ "$HTTP_CODE" != "200" && "$HTTP_CODE" != "201" ]]; then + echo "❌ Fehler beim Erstellen des Adminusers (HTTP $HTTP_CODE):" + echo "$BODY" | jq . + exit 1 +fi +echo "✅ Adminuser erfolgreich erstellt:" +echo "$BODY" | jq . +USERID=$(echo "$BODY" | jq -r '.id') + +# Create admin-user +echo "Add user to organisation" +CREATE_RESPONSE=$(curl -s -w "\n%{http_code}" --location \ + --request POST "$LOGTO_API_URL/api/organizations/t-default/users" \ + --header "Authorization: Bearer $ACCESS_TOKEN" \ + --header "Content-Type: application/json" \ + --data "{ + \"userIds\": [\"$USERID\"] + }") +HTTP_CODE=$(echo "$CREATE_RESPONSE" | tail -n1) +BODY=$(echo "$CREATE_RESPONSE" | sed '$d') +if [[ "$HTTP_CODE" != "200" && "$HTTP_CODE" != "201" ]]; then + echo "❌ Fehler beim Hinzufügen des Adminusers zur Organisation (HTTP $HTTP_CODE):" + echo "$BODY" | jq . + exit 1 +fi +echo "✅ Adminuser erfolgreich hinzugefügt:" + + +echo "Adminrechte der Organisation zugeweisen" +CREATE_RESPONSE=$(curl -s -w "\n%{http_code}" --location \ + --request POST "$LOGTO_API_URL/api/organizations/t-default/users/roles" \ + --header "Authorization: Bearer $ACCESS_TOKEN" \ + --header "Content-Type: application/json" \ + --data "{ + \"userIds\": [\"$USERID\"], + \"organizationRoleIds\": [\"admin\"] + }") +HTTP_CODE=$(echo "$CREATE_RESPONSE" | tail -n1) +BODY=$(echo "$CREATE_RESPONSE" | sed '$d') +if [[ "$HTTP_CODE" != "200" && "$HTTP_CODE" != "201" ]]; then + echo "❌ Fehler beim Hinzufügen der Adminrechte zur Organisation (HTTP $HTTP_CODE):" + echo "$BODY" | jq . + exit 1 +fi +echo "✅ Adminrechte erfolgreich hinzugefügt:" + +echo "Rollen laden" +CREATE_RESPONSE=$(curl -s -w "\n%{http_code}" --location \ + --request GET "$LOGTO_API_URL/api/roles?type=User" \ + --header "Authorization: Bearer $ACCESS_TOKEN") +HTTP_CODE=$(echo "$CREATE_RESPONSE" | tail -n1) +BODY=$(echo "$CREATE_RESPONSE" | sed '$d') +if [[ "$HTTP_CODE" != "200" && "$HTTP_CODE" != "201" ]]; then + echo "❌ Fehler beim Laden der Rollen (HTTP $HTTP_CODE):" + echo "$BODY" | jq . + exit 1 +fi +echo "✅ Rollen erfolgreich geladen:" +echo "$BODY" | jq . +ROLE_IDS=$(echo "$BODY" | jq -r '.[].id' | jq -R . | paste -sd, -) +echo "Gefundene Rollen IDs: $ROLE_IDS" + +echo "Rollen zu Adminuser zuweisen" +CREATE_RESPONSE=$(curl -s -w "\n%{http_code}" --location \ + --request POST "$LOGTO_API_URL/api/users/$USERID/roles" \ + --header "Authorization: Bearer $ACCESS_TOKEN" \ + --header "Content-Type: application/json" \ + --data "{ + \"roleIds\": [$(echo "$ROLE_IDS" | paste -sd, -)] + }") +HTTP_CODE=$(echo "$CREATE_RESPONSE" | tail -n1) +BODY=$(echo "$CREATE_RESPONSE" | sed '$d') +if [[ "$HTTP_CODE" != "200" && "$HTTP_CODE" != "201" ]]; then + echo "❌ Fehler beim Zuweisen der Rollen (HTTP $HTTP_CODE):" + echo "$BODY" | jq . + exit 1 +fi +echo "✅ Rollen erfolgreich zugewiesen:" + +echo "Login anpassen" +CREATE_RESPONSE=$(curl -s -w "\n%{http_code}" --location \ + --request PATCH "$LOGTO_API_URL/api/sign-in-exp" \ + --header "Authorization: Bearer $ACCESS_TOKEN" \ + --header "Content-Type: application/json" \ + --data "{ + \"tenantId\": \"admin\", + \"signInMode\": \"SignIn\" + }") +HTTP_CODE=$(echo "$CREATE_RESPONSE" | tail -n1) +BODY=$(echo "$CREATE_RESPONSE" | sed '$d') +if [[ "$HTTP_CODE" != "200" && "$HTTP_CODE" != "201" ]]; then + echo "❌ Fehler beim Anpassen des Logins (HTTP $HTTP_CODE):" + echo "$BODY" | jq . + exit 1 +fi +echo "✅ Login erfolgreich angepasst:" \ No newline at end of file diff --git a/logto.md b/logto.md new file mode 100644 index 00000000..a89986bf --- /dev/null +++ b/logto.md @@ -0,0 +1,73 @@ +curl --location \ + --request POST 'http://localhost:3001/oidc/token' \ + --header 'Authorization: Basic MHN4ZTd3NWV1eGdqcDFrZnJid3g3OlVueVp3d0RCY2gzUjA1NTRzcUJRR0VuSWVjU0hyMXk5' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'grant_type=client_credentials' \ + --data-urlencode 'resource=https://default.logto.app/api' \ + --data-urlencode 'scope=all' + +------- + +RFtlQNmRxH4HLDMZVra6Zad0VeWNsT8a + +curl --location \ + --request POST 'http://localhost:3002/oidc/token' \ + --header 'Authorization: Basic bS1hZG1pbjpSRnRsUU5tUnhINEhMRE1aVnJhNlphZDBWZVdOc1Q4YQ' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'grant_type=client_credentials' \ + --data-urlencode 'resource=https://default.logto.app/api' \ + --data-urlencode 'scope=all' + +curl \ + --request POST 'http://localhost:3001/api/applications' \ + --header "Authorization: Bearer RFtlQNmRxH4HLDMZVra6Zad0VeWNsT8a" \ + --header "Content-Type: application/json" \ + --data '{"name":"mini-lab","description":"Mini-Lab","type":"MachineToMachine"}' + + +---- + +./logto-create-admin --baseUrl=http://localhost:3002 --appSecret=RFtlQNmRxH4HLDMZVra6Zad0VeWNsT8a --username=admin --password=password123 + +---- + +Solution: + +1. Get m-admin token +docker exec -it logto-postgres-1 sh -c 'psql -U postgres -d logto -t -A -c "SELECT secret FROM applications WHERE id = '\''m-admin'\'';"' + +2. Use the token to get access token for m-admin +curl --location \ + --request POST 'http://localhost:3002/oidc/token' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'grant_type=client_credentials' \ + --data-urlencode 'client_id=m-admin' \ + --data-urlencode 'client_secret=RFtlQNmRxH4HLDMZVra6Zad0VeWNsT8a' \ + --data-urlencode 'resource=https://admin.logto.app/api' \ + --data-urlencode 'scope=all' \ +| jq -r '.access_token' + +3. Use the access token to manage entities +curl --location \ + --request GET 'http://localhost:3002/api/applications?isThirdParty=true' \ + --header 'Authorization: Bearer eyJhbGciOiJFUzM4NCIsInR5cCI6ImF0K2p3dCIsImtpZCI6IjlobWw4NDl5NUZYQk5mUE93bnA1Q1g3ZUVkdERTejl5ejd5SllOZ0RnajAifQ.eyJqdGkiOiI5aFZDZWlaV202Q1p4Z0VDRTJWVDIiLCJzdWIiOiJtLWFkbWluIiwiaWF0IjoxNzYxNzQ0NjA1LCJleHAiOjE3NjE3NDgyMDUsInNjb3BlIjoiYWxsIiwiY2xpZW50X2lkIjoibS1hZG1pbiIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMi9vaWRjIiwiYXVkIjoiaHR0cHM6Ly9hZG1pbi5sb2d0by5hcHAvYXBpIn0.u7GEcRma56PDiFSPF4_281xtodMUD1ZlpQu_NNAWKhpAj5RAg_zZKFk7sR3euXk3mgPqjko2oPBBTbDh9i0hiwjRDY-Iv_pYDlD9L18xUbjjIyoPI6X3hqTGNXpK-u0t' + +4. Create mini-lab oidc app +curl --location \ + --request POST 'http://localhost:3002/api/applications' \ + --header 'Authorization: Bearer eyJhbGciOiJFUzM4NCIsInR5cCI6ImF0K2p3dCIsImtpZCI6IjlobWw4NDl5NUZYQk5mUE93bnA1Q1g3ZUVkdERTejl5ejd5SllOZ0RnajAifQ.eyJqdGkiOiI5aFZDZWlaV202Q1p4Z0VDRTJWVDIiLCJzdWIiOiJtLWFkbWluIiwiaWF0IjoxNzYxNzQ0NjA1LCJleHAiOjE3NjE3NDgyMDUsInNjb3BlIjoiYWxsIiwiY2xpZW50X2lkIjoibS1hZG1pbiIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMi9vaWRjIiwiYXVkIjoiaHR0cHM6Ly9hZG1pbi5sb2d0by5hcHAvYXBpIn0.u7GEcRma56PDiFSPF4_281xtodMUD1ZlpQu_NNAWKhpAj5RAg_zZKFk7sR3euXk3mgPqjko2oPBBTbDh9i0hiwjRDY-Iv_pYDlD9L18xUbjjIyoPI6X3hqTGNXpK-u0t' \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "mini-lab", + "description": "mini-lab IdP", + "type": "Traditional", + "oidcClientMetadata": { + "redirectUris": [ + "http://v2.api.172.17.0.1.nip.io:8080/auth/oidc/callback" + ], + "postLogoutRedirectUris": [ + "http://v2.api.172.17.0.1.nip.io:8080/auth/oidc/callback" + ] + }, + "isThirdParty": true + }' diff --git a/roles/logto/README.md b/roles/logto/README.md new file mode 100644 index 00000000..1a101a64 --- /dev/null +++ b/roles/logto/README.md @@ -0,0 +1,59 @@ +Role Name +========= + +A brief description of the role goes here. + +## Notes + +Well known config for the apiserver +# http://localhost:3001/oidc/.well-known/openid-configuration + + +Machine2Machine Account +https://docs.logto.io/integrate-logto/interact-with-management-api + +```bash +curl --location \ + --request POST 'http://logto.172.17.0.1.nip.io:8080' \ + --header 'Authorization: Basic a3FxNm5tWmpRdVZkQzJPOHpWOUozR2dqRnF2Y09aWUEK' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'grant_type=client_credentials' \ + --data-urlencode 'resource=https://default.logto.app/api' \ + --data-urlencode 'scope=all' +``` + +Does not work yet + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +MIT + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/roles/logto/defaults/main.yml b/roles/logto/defaults/main.yml new file mode 100644 index 00000000..7cd671b8 --- /dev/null +++ b/roles/logto/defaults/main.yml @@ -0,0 +1,2 @@ +--- +logto_namespace: "metal-control-plane" \ No newline at end of file diff --git a/roles/logto/tasks/main.yml b/roles/logto/tasks/main.yml new file mode 100644 index 00000000..5341ac76 --- /dev/null +++ b/roles/logto/tasks/main.yml @@ -0,0 +1,58 @@ +--- +- name: Git clone logto repo + ansible.builtin.git: + repo: "https://github.com/majst01/logto-helm.git" + dest: /tmp/helm/logto + single_branch: yes + version: use-official-kubectl-image + +- name: Deploy tls secret for logto admin ingress + k8s: + definition: "{{ lookup('template', 'logto-admin-tls.yaml') }}" + namespace: "{{ logto_namespace }}" + apply: true + +- name: Deploy postgres + k8s: + definition: "{{ lookup('template', 'postgres.yaml') }}" + namespace: "{{ logto_namespace }}" + apply: true + +- name: Deploy logto + k8s: + definition: "{{ lookup('template', 'logto.yaml') }}" + namespace: "{{ logto_namespace }}" + apply: true + +- name: Deploy secret-extractor + k8s: + definition: "{{ lookup('template', 'secret-extractor.yaml') }}" + namespace: "{{ logto_namespace }}" + apply: true + +- name: Wait for logto secret-extractor job to complete + kubernetes.core.k8s_info: + api_version: batch/v1 + kind: Job + name: logto-secret-extractor + namespace: "{{ logto_namespace }}" + register: logto_job_info + until: logto_job_info.resources[0].status.succeeded | default(0) > 0 + retries: 30 + delay: 10 + +- name: Get logto secret + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + name: logto-application-secrets + namespace: metal-control-plane + register: logto_secret + +- name: Decode m-admin-secret from base64 + set_fact: + logto_m_admin_secret: "{{ logto_secret.resources[0].data['m-admin-secret'] | b64decode | trim }}" + +- name: Show access token + debug: + msg: "Access Token: {{ logto_m_admin_secret }}" \ No newline at end of file diff --git a/roles/logto/templates/logto-admin-tls.yaml b/roles/logto/templates/logto-admin-tls.yaml new file mode 100644 index 00000000..ae30967c --- /dev/null +++ b/roles/logto/templates/logto-admin-tls.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: logto-admin-tls +type: kubernetes.io/tls +data: + tls.key: {{ logto_admin_certs_server_key | b64encode }} + tls.crt: {{ logto_admin_certs_server_cert | b64encode }} + ca.crt: {{ logto_admin_certs_ca | b64encode }} \ No newline at end of file diff --git a/roles/logto/templates/logto.yaml b/roles/logto/templates/logto.yaml new file mode 100644 index 00000000..2748b083 --- /dev/null +++ b/roles/logto/templates/logto.yaml @@ -0,0 +1,136 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: logto + labels: + app.kubernetes.io/name: logto +--- +apiVersion: v1 +kind: Service +metadata: + name: logto + labels: + app.kubernetes.io/name: logto +spec: + type: ClusterIP + ports: + - port: 3001 + targetPort: 3001 + protocol: TCP + name: http-main + - port: 3002 + targetPort: 3002 + protocol: TCP + name: http-admin + selector: + app.kubernetes.io/name: logto +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: logto + labels: + app.kubernetes.io/name: logto +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: logto + template: + metadata: + labels: + app.kubernetes.io/name: logto + spec: + serviceAccountName: logto + initContainers: + - name: wait-for-db + image: postgres:17-alpine + command: ['sh', '-c', 'until pg_isready -h logto-postgresql -p 5432; do echo waiting for postgresql; sleep 2; done;'] + containers: + - name: logto + image: svhd/logto:latest + imagePullPolicy: IfNotPresent + command: ["sh", "-c", "npm run cli db seed -- --swe && npm start"] + ports: + - name: http-main + containerPort: 3001 + protocol: TCP + - name: http-admin + containerPort: 3002 + protocol: TCP + volumeMounts: + - name: logto-admin-ca + mountPath: /etc/ssl/certs/ca.crt + subPath: ca.crt + readOnly: true + env: + - name: NODE_TLS_REJECT_UNAUTHORIZED + value: "0" + - name: TRUST_PROXY_HEADER + value: "true" + - name: DB_URL + value: postgres://postgres:postgres@logto-postgresql:5432/logto + - name: ENDPOINT + value: https://logto.172.17.0.1.nip.io:4443 + - name: ADMIN_ENDPOINT + value: https://logto-admin.172.17.0.1.nip.io:4443 + # Changed health check path to /api/health which is more common + livenessProbe: + httpGet: + path: /api/status + port: 3001 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + readinessProbe: + httpGet: + path: /api/status + port: 3001 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + limits: + cpu: 1000m + memory: 1024Mi + requests: + cpu: 100m + memory: 128Mi +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: logto + labels: + app.kubernetes.io/name: logto +spec: + ingressClassName: nginx + tls: + - hosts: + - logto.172.17.0.1.nip.io + - logto-admin.172.17.0.1.nip.io + secretName: logto-admin-tls + rules: + - host: logto.172.17.0.1.nip.io + http: + paths: + - backend: + service: + name: logto + port: + name: http-main + path: / + pathType: ImplementationSpecific + - host: logto-admin.172.17.0.1.nip.io + http: + paths: + - backend: + service: + name: logto + port: + name: http-admin + path: / + pathType: ImplementationSpecific \ No newline at end of file diff --git a/roles/logto/templates/postgres.yaml b/roles/logto/templates/postgres.yaml new file mode 100644 index 00000000..a0786a11 --- /dev/null +++ b/roles/logto/templates/postgres.yaml @@ -0,0 +1,97 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: logto-postgresql + labels: + app.kubernetes.io/component: database +spec: + type: ClusterIP + ports: + - port: 5432 + targetPort: postgresql + protocol: TCP + name: postgresql + selector: + app.kubernetes.io/component: database +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: logto-postgresql + labels: + app.kubernetes.io/component: database +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 8Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: logto-postgresql + labels: + app.kubernetes.io/name: logto + app.kubernetes.io/component: database +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: logto + app.kubernetes.io/component: database + template: + metadata: + labels: + app.kubernetes.io/name: logto + app.kubernetes.io/component: database + spec: + containers: + - name: postgresql + image: postgres:17-alpine + imagePullPolicy: IfNotPresent + ports: + - name: postgresql + containerPort: 5432 + protocol: TCP + env: + - name: POSTGRES_USER + value: postgres + - name: POSTGRES_PASSWORD + value: postgres + - name: POSTGRES_DB + value: logto + livenessProbe: + exec: + command: + - pg_isready + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + exec: + command: + - pg_isready + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 3 + resources: + limits: + cpu: 1000m + memory: 1024Mi + requests: + cpu: 100m + memory: 128Mi + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + volumes: + - name: data + persistentVolumeClaim: + claimName: logto-postgresql +--- \ No newline at end of file diff --git a/roles/logto/templates/secret-extractor.yaml b/roles/logto/templates/secret-extractor.yaml new file mode 100644 index 00000000..068df1fc --- /dev/null +++ b/roles/logto/templates/secret-extractor.yaml @@ -0,0 +1,134 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: logto-secret-extractor + labels: + app.kubernetes.io/component: secret-extractor +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: logto-secret-extractor + labels: + app.kubernetes.io/component: secret-extractor +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "create", "update", "patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: logto-secret-extractor + labels: + app.kubernetes.io/component: secret-extractor +subjects: + - kind: ServiceAccount + name: logto-secret-extractor + namespace: metal-control-plane +roleRef: + kind: Role + name: logto-secret-extractor + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: logto-secret-extractor +spec: + ttlSecondsAfterFinished: 100 + template: + metadata: + labels: + app.kubernetes.io/name: logto + app.kubernetes.io/component: secret-extractor + spec: + serviceAccountName: logto-secret-extractor + restartPolicy: OnFailure + volumes: + - name: shared-data + emptyDir: {} + containers: + - name: secret-extractor + image: postgres:17-alpine + env: + - name: POSTGRES_HOST + value: logto-postgresql + - name: POSTGRES_USER + value: postgres + - name: POSTGRES_PASSWORD + value: postgres + - name: POSTGRES_DB + value: logto + volumeMounts: + - name: shared-data + mountPath: /shared + command: + - /bin/sh + - -c + - | + # Wait for PostgreSQL to be ready + until PGPASSWORD=$POSTGRES_PASSWORD psql -h $POSTGRES_HOST -U $POSTGRES_USER -d $POSTGRES_DB -c "SELECT 1;" > /dev/null 2>&1; do + echo "Waiting for PostgreSQL to be ready..." + sleep 2 + done + + # Wait for both applications to exist and extract their secrets + while true; do + # Check if both applications exist and get their secrets + SECRETS=$(PGPASSWORD=$POSTGRES_PASSWORD psql -h $POSTGRES_HOST -U $POSTGRES_USER -d $POSTGRES_DB -t -A -F"," -c \ + "SELECT id, secret FROM applications WHERE id IN ('m-default', 'm-admin');") + + COUNT=$(echo "$SECRETS" | wc -l) + if [ "$COUNT" -eq 2 ]; then + echo "Found both applications, extracting secrets..." + break + fi + + echo "Waiting for applications to be created..." + sleep 5 + done + + # Extract and base64 encode secrets + M_DEFAULT_SECRET=$(echo "$SECRETS" | grep "^m-default," | cut -d',' -f2 | base64) + M_ADMIN_SECRET=$(echo "$SECRETS" | grep "^m-admin," | cut -d',' -f2 | base64) + + # Create Kubernetes Secret manifest + cat < /shared/secret.yaml + apiVersion: v1 + kind: Secret + metadata: + name: logto-application-secrets + type: Opaque + data: + m-default-secret: $M_DEFAULT_SECRET + m-admin-secret: $M_ADMIN_SECRET + EOF + + # Signal that the secret file is ready + touch /shared/secret-ready + + # Wait for the secret to be applied + while [ -f /shared/secret-ready ]; do + sleep 1 + done + - name: kubectl + image: "bitnami/kubectl:latest" + volumeMounts: + - name: shared-data + mountPath: /shared + command: + - /bin/sh + - -c + - | + # Wait for the secret file to be ready + while [ ! -f /shared/secret-ready ]; do + sleep 1 + done + + # Apply the secret + kubectl apply -f /shared/secret.yaml + + # Signal completion + rm -f /shared/secret-ready \ No newline at end of file diff --git a/scripts/roll_certs.sh b/scripts/roll_certs.sh index 7fc34acd..51b30dec 100755 --- a/scripts/roll_certs.sh +++ b/scripts/roll_certs.sh @@ -13,6 +13,7 @@ rm *.csr for component in \ grpc \ + logto-admin \ masterdata-api; do pushd $component