Rebuild auth on Keycloak OIDC, fix rootless Ansible deploy
Replaces the single shared HTTP Basic service-account credential (which caused a production outage from a username mismatch) with per-user login: Keycloak (already running on this VM for gcnm, now also fronted on auth.friessn.de with its own "homekeeper" realm) authenticates the user once via the landing page, FastAPI verifies the OIDC id_token and mints its own signed session JWT as a cookie, and both Shiny apps forward that per-session token as a Bearer credential instead of a static shared one. Authorization is a simple ALLOWED_USERS allowlist; the old auth.users table and bcrypt seeding are gone entirely. Also carries forward the in-progress rootless Podman/Quadlet migration (gitea, homekeeper, podman roles) and fixes a pre-existing bug where each role's handlers were malformed inside tasks/main.yml instead of their own handlers/main.yml, which broke ansible-playbook entirely. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbiCdckkTX2HyAkyi1R39d
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
---
|
||||
- name: Reload gitea user systemd
|
||||
become_user: "{{ deploy_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ deploy_uid }}"
|
||||
systemd:
|
||||
daemon_reload: true
|
||||
scope: user
|
||||
|
||||
- name: Restart gitea
|
||||
become_user: "{{ deploy_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ deploy_uid }}"
|
||||
systemd:
|
||||
name: gitea
|
||||
state: restarted
|
||||
enabled: true
|
||||
scope: user
|
||||
@@ -1,26 +1,24 @@
|
||||
---
|
||||
- name: Create Gitea quadlet directory
|
||||
# Gitea runs rootless as {{ deploy_user }} — see group_vars/all.yml.
|
||||
- name: Enable linger for {{ deploy_user }} (user services survive logout/reboot)
|
||||
command: "loginctl enable-linger {{ deploy_user }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Create user Quadlet directory
|
||||
file:
|
||||
path: /etc/containers/systemd
|
||||
path: "/home/{{ deploy_user }}/.config/containers/systemd"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
owner: "{{ deploy_user }}"
|
||||
group: "{{ deploy_user }}"
|
||||
|
||||
- name: Deploy Gitea container quadlet
|
||||
template:
|
||||
src: gitea.container.j2
|
||||
dest: /etc/containers/systemd/gitea.container
|
||||
dest: "/home/{{ deploy_user }}/.config/containers/systemd/gitea.container"
|
||||
owner: "{{ deploy_user }}"
|
||||
group: "{{ deploy_user }}"
|
||||
mode: "0644"
|
||||
notify:
|
||||
- Reload systemd
|
||||
- Reload gitea user systemd
|
||||
- Restart gitea
|
||||
|
||||
handlers:
|
||||
- name: Reload systemd
|
||||
systemd:
|
||||
daemon_reload: true
|
||||
|
||||
- name: Restart gitea
|
||||
systemd:
|
||||
name: gitea
|
||||
state: restarted
|
||||
enabled: true
|
||||
|
||||
@@ -6,11 +6,13 @@ After=network-online.target
|
||||
Image=docker.io/gitea/gitea:latest
|
||||
ContainerName=gitea
|
||||
PublishPort=127.0.0.1:{{ gitea_http_port }}:3000
|
||||
Volume={{ gitea_data_dir }}:/data:Z
|
||||
Environment=USER_UID=1000
|
||||
Environment=USER_GID=1000
|
||||
PublishPort={{ gitea_ssh_port }}:22
|
||||
Volume={{ gitea_data_volume }}:/data
|
||||
Environment=USER_UID={{ deploy_uid }}
|
||||
Environment=USER_GID={{ deploy_uid }}
|
||||
Environment=GITEA__server__DOMAIN={{ gitea_domain }}
|
||||
Environment=GITEA__server__ROOT_URL=https://{{ gitea_domain }}/
|
||||
Environment=GITEA__server__SSH_DOMAIN={{ gitea_domain }}
|
||||
Environment=GITEA__server__HTTP_PORT=3000
|
||||
Environment=GITEA__packages__ENABLED=true
|
||||
Environment=GITEA__packages__CHUNKED_UPLOAD_PATH=/data/tmp/package-upload
|
||||
@@ -20,4 +22,4 @@ Restart=always
|
||||
TimeoutStartSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target default.target
|
||||
WantedBy=default.target
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
- name: Reload homekeeper user systemd
|
||||
become_user: "{{ deploy_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ deploy_uid }}"
|
||||
systemd:
|
||||
daemon_reload: true
|
||||
scope: user
|
||||
|
||||
- name: Restart homekeeper
|
||||
become_user: "{{ deploy_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ deploy_uid }}"
|
||||
systemd:
|
||||
name: "{{ item }}"
|
||||
state: restarted
|
||||
enabled: true
|
||||
scope: user
|
||||
loop:
|
||||
- homekeeper-db
|
||||
- homekeeper-api
|
||||
- homekeeper-beekeeper
|
||||
- homekeeper-listkeeper
|
||||
@@ -1,21 +1,28 @@
|
||||
---
|
||||
- name: Create quadlet directory
|
||||
# Homekeeper containers run rootless as {{ deploy_user }}, same pattern as the gitea role.
|
||||
- name: Create user Quadlet directory
|
||||
file:
|
||||
path: /etc/containers/systemd
|
||||
path: "/home/{{ deploy_user }}/.config/containers/systemd"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
owner: "{{ deploy_user }}"
|
||||
group: "{{ deploy_user }}"
|
||||
|
||||
- name: Deploy homekeeper network quadlet
|
||||
template:
|
||||
src: homekeeper.network.j2
|
||||
dest: /etc/containers/systemd/homekeeper.network
|
||||
dest: "/home/{{ deploy_user }}/.config/containers/systemd/homekeeper.network"
|
||||
owner: "{{ deploy_user }}"
|
||||
group: "{{ deploy_user }}"
|
||||
mode: "0644"
|
||||
notify: Reload systemd
|
||||
notify: Reload homekeeper user systemd
|
||||
|
||||
- name: Deploy container quadlets
|
||||
template:
|
||||
src: "{{ item }}.j2"
|
||||
dest: "/etc/containers/systemd/{{ item }}"
|
||||
dest: "/home/{{ deploy_user }}/.config/containers/systemd/{{ item }}"
|
||||
owner: "{{ deploy_user }}"
|
||||
group: "{{ deploy_user }}"
|
||||
mode: "0644"
|
||||
loop:
|
||||
- homekeeper-db.container
|
||||
@@ -23,21 +30,5 @@
|
||||
- homekeeper-beekeeper.container
|
||||
- homekeeper-listkeeper.container
|
||||
notify:
|
||||
- Reload systemd
|
||||
- Reload homekeeper user systemd
|
||||
- Restart homekeeper
|
||||
|
||||
handlers:
|
||||
- name: Reload systemd
|
||||
systemd:
|
||||
daemon_reload: true
|
||||
|
||||
- name: Restart homekeeper
|
||||
systemd:
|
||||
name: "{{ item }}"
|
||||
state: restarted
|
||||
enabled: true
|
||||
loop:
|
||||
- homekeeper-db
|
||||
- homekeeper-api
|
||||
- homekeeper-beekeeper
|
||||
- homekeeper-listkeeper
|
||||
|
||||
@@ -13,7 +13,14 @@ Environment=DB_NAME={{ db_name }}
|
||||
Environment=DB_USER={{ db_user }}
|
||||
Environment=DB_PASSWORD={{ db_password }}
|
||||
Environment=ROOT_PATH=/api
|
||||
Environment=INITIAL_USERS={{ initial_users }}
|
||||
Environment=ENV=production
|
||||
Environment=COOKIE_SECURE=true
|
||||
Environment=PUBLIC_BASE_URL=https://{{ domain }}
|
||||
Environment=OIDC_ISSUER_URL={{ oidc_issuer_url }}
|
||||
Environment=OIDC_CLIENT_ID={{ oidc_client_id }}
|
||||
Environment=OIDC_CLIENT_SECRET={{ oidc_client_secret }}
|
||||
Environment=SESSION_JWT_SECRET={{ session_jwt_secret }}
|
||||
Environment=ALLOWED_USERS={{ allowed_users }}
|
||||
AutoUpdate=registry
|
||||
Label=io.containers.autoupdate=registry
|
||||
|
||||
@@ -22,4 +29,4 @@ Restart=always
|
||||
TimeoutStartSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target default.target
|
||||
WantedBy=default.target
|
||||
|
||||
@@ -8,8 +8,6 @@ ContainerName=homekeeper-beekeeper
|
||||
Network=homekeeper.network
|
||||
PublishPort=127.0.0.1:3838:3838
|
||||
Environment=API_URL=http://homekeeper-api:8000
|
||||
Environment=API_USER={{ api_user }}
|
||||
Environment=API_PASS={{ api_pass }}
|
||||
AutoUpdate=registry
|
||||
Label=io.containers.autoupdate=registry
|
||||
|
||||
@@ -18,4 +16,4 @@ Restart=always
|
||||
TimeoutStartSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target default.target
|
||||
WantedBy=default.target
|
||||
|
||||
@@ -6,7 +6,7 @@ After=network-online.target
|
||||
Image=docker.io/library/postgres:17
|
||||
ContainerName=homekeeper-db
|
||||
Network=homekeeper.network
|
||||
Volume={{ homekeeper_data_dir }}/pg_data:/var/lib/postgresql/data:Z
|
||||
Volume={{ homekeeper_db_volume }}:/var/lib/postgresql/data
|
||||
Environment=POSTGRES_DB={{ db_name }}
|
||||
Environment=POSTGRES_USER={{ db_user }}
|
||||
Environment=POSTGRES_PASSWORD={{ db_password }}
|
||||
@@ -20,4 +20,4 @@ Restart=always
|
||||
TimeoutStartSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target default.target
|
||||
WantedBy=default.target
|
||||
|
||||
@@ -9,8 +9,6 @@ Network=homekeeper.network
|
||||
PublishPort=127.0.0.1:3839:3839
|
||||
Environment=PORT=3839
|
||||
Environment=API_URL=http://homekeeper-api:8000
|
||||
Environment=API_USER={{ api_user }}
|
||||
Environment=API_PASS={{ api_pass }}
|
||||
AutoUpdate=registry
|
||||
Label=io.containers.autoupdate=registry
|
||||
|
||||
@@ -19,4 +17,4 @@ Restart=always
|
||||
TimeoutStartSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target default.target
|
||||
WantedBy=default.target
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
- name: Reload nginx
|
||||
service:
|
||||
name: nginx
|
||||
state: reloaded
|
||||
@@ -14,37 +14,63 @@
|
||||
state: absent
|
||||
notify: Reload nginx
|
||||
|
||||
- name: Deploy homekeeper nginx config
|
||||
# git.friessn.de already has its own site file (deployed manually before this
|
||||
# role existed, same one-file-per-domain convention as the other sites on this
|
||||
# box) — this role only manages home.friessn.de.
|
||||
- name: Deploy home.friessn.de nginx config
|
||||
template:
|
||||
src: homekeeper.conf.j2
|
||||
dest: /etc/nginx/sites-available/homekeeper.conf
|
||||
src: home.friessn.de.conf.j2
|
||||
dest: /etc/nginx/sites-available/home.friessn.de
|
||||
mode: "0644"
|
||||
notify: Reload nginx
|
||||
|
||||
- name: Enable homekeeper nginx site
|
||||
- name: Enable home.friessn.de nginx site
|
||||
file:
|
||||
src: /etc/nginx/sites-available/homekeeper.conf
|
||||
dest: /etc/nginx/sites-enabled/homekeeper.conf
|
||||
src: /etc/nginx/sites-available/home.friessn.de
|
||||
dest: /etc/nginx/sites-enabled/home.friessn.de
|
||||
state: link
|
||||
notify: Reload nginx
|
||||
|
||||
- name: Obtain Let's Encrypt certificates
|
||||
- name: Obtain Let's Encrypt certificate for {{ domain }}
|
||||
command: >
|
||||
certbot --nginx -d {{ domain }} -d {{ gitea_domain }}
|
||||
certbot --nginx -d {{ domain }}
|
||||
--non-interactive --agree-tos -m {{ gitea_admin_email }}
|
||||
--redirect
|
||||
args:
|
||||
creates: /etc/letsencrypt/live/{{ domain }}/fullchain.pem
|
||||
notify: Reload nginx
|
||||
|
||||
# Keycloak itself runs as a plain Docker container for the unrelated gcnm
|
||||
# app (not managed by this role) — it's already published on
|
||||
# 127.0.0.1:8080, this just fronts it with TLS on its own subdomain so
|
||||
# Homekeeper (and anything else on the box) can treat it as a normal OIDC
|
||||
# provider. Requires a DNS record for {{ keycloak_domain }} pointing at this
|
||||
# VM before the certbot step below can succeed.
|
||||
- name: Deploy auth.friessn.de nginx config
|
||||
template:
|
||||
src: auth.friessn.de.conf.j2
|
||||
dest: /etc/nginx/sites-available/{{ keycloak_domain }}
|
||||
mode: "0644"
|
||||
notify: Reload nginx
|
||||
|
||||
- name: Enable auth.friessn.de nginx site
|
||||
file:
|
||||
src: /etc/nginx/sites-available/{{ keycloak_domain }}
|
||||
dest: /etc/nginx/sites-enabled/{{ keycloak_domain }}
|
||||
state: link
|
||||
notify: Reload nginx
|
||||
|
||||
- name: Obtain Let's Encrypt certificate for {{ keycloak_domain }}
|
||||
command: >
|
||||
certbot --nginx -d {{ keycloak_domain }}
|
||||
--non-interactive --agree-tos -m {{ gitea_admin_email }}
|
||||
--redirect
|
||||
args:
|
||||
creates: /etc/letsencrypt/live/{{ keycloak_domain }}/fullchain.pem
|
||||
notify: Reload nginx
|
||||
|
||||
- name: Enable nginx
|
||||
service:
|
||||
name: nginx
|
||||
enabled: true
|
||||
state: started
|
||||
|
||||
handlers:
|
||||
- name: Reload nginx
|
||||
service:
|
||||
name: nginx
|
||||
state: reloaded
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# ── Keycloak (shared identity provider — homekeeper realm lives here,
|
||||
# the gcnm app has its own separate realm): {{ keycloak_domain }} ─────────
|
||||
server {
|
||||
listen 80;
|
||||
server_name {{ keycloak_domain }};
|
||||
# certbot --nginx adds SSL redirect + listen 443 block here
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
}
|
||||
+1
-19
@@ -3,7 +3,7 @@ map $http_upgrade $connection_upgrade {
|
||||
'' close;
|
||||
}
|
||||
|
||||
# ── Main app: friessn.de ────────────────────────────────────────────────────
|
||||
# ── Main app: {{ domain }} ───────────────────────────────────────────────────
|
||||
server {
|
||||
listen 80;
|
||||
server_name {{ domain }};
|
||||
@@ -57,21 +57,3 @@ server {
|
||||
proxy_send_timeout 86400s;
|
||||
}
|
||||
}
|
||||
|
||||
# ── Gitea: git.friessn.de ───────────────────────────────────────────────────
|
||||
server {
|
||||
listen 80;
|
||||
server_name {{ gitea_domain }};
|
||||
# certbot --nginx adds SSL redirect + listen 443 block here
|
||||
|
||||
client_max_body_size 512m;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:{{ gitea_http_port }};
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
- name: Reload systemd
|
||||
systemd:
|
||||
daemon_reload: true
|
||||
|
||||
- name: Restart podman services
|
||||
command: systemctl daemon-reload
|
||||
|
||||
- name: Reload user systemd
|
||||
become_user: "{{ deploy_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ deploy_uid }}"
|
||||
systemd:
|
||||
daemon_reload: true
|
||||
scope: user
|
||||
@@ -4,18 +4,14 @@
|
||||
name:
|
||||
- podman
|
||||
- podman-compose # for ad-hoc use; production uses quadlets
|
||||
- slirp4netns # rootless networking / port publishing
|
||||
- uidmap # rootless subuid/subgid mapping
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Create homekeeper data directories
|
||||
file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
mode: "0750"
|
||||
loop:
|
||||
- "{{ homekeeper_data_dir }}"
|
||||
- "{{ homekeeper_data_dir }}/pg_data"
|
||||
- "{{ gitea_data_dir }}"
|
||||
# All app data lives in named Podman volumes (created implicitly on first
|
||||
# `podman run`/Quadlet start), not host bind mounts — avoids rootless UID
|
||||
# mapping headaches. Nothing to pre-create here.
|
||||
|
||||
- name: Configure Gitea as additional registry
|
||||
template:
|
||||
@@ -25,35 +21,46 @@
|
||||
notify: Restart podman services
|
||||
|
||||
- name: Login to Gitea container registry
|
||||
become_user: "{{ deploy_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ deploy_uid }}"
|
||||
command: >
|
||||
podman login {{ registry_host }}:{{ registry_port }}
|
||||
podman login {{ registry_host }}
|
||||
-u {{ registry_user }} -p {{ registry_token }}
|
||||
register: login_result
|
||||
changed_when: "'Login Succeeded' in login_result.stdout"
|
||||
# Run this after Gitea is up and registry_token is set
|
||||
|
||||
- name: Enable podman-auto-update timer
|
||||
- name: Enable linger for {{ deploy_user }} (user services survive logout/reboot)
|
||||
command: "loginctl enable-linger {{ deploy_user }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Enable podman-auto-update timer (user scope)
|
||||
become_user: "{{ deploy_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ deploy_uid }}"
|
||||
systemd:
|
||||
name: podman-auto-update.timer
|
||||
enabled: true
|
||||
state: started
|
||||
scope: user
|
||||
daemon_reload: true
|
||||
|
||||
- name: Override auto-update timer schedule
|
||||
- name: Override auto-update timer schedule (user scope)
|
||||
become_user: "{{ deploy_user }}"
|
||||
file:
|
||||
path: "/home/{{ deploy_user }}/.config/systemd/user/podman-auto-update.timer.d"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Deploy auto-update timer schedule override
|
||||
become_user: "{{ deploy_user }}"
|
||||
copy:
|
||||
dest: /etc/systemd/system/podman-auto-update.timer.d/override.conf
|
||||
dest: "/home/{{ deploy_user }}/.config/systemd/user/podman-auto-update.timer.d/override.conf"
|
||||
content: |
|
||||
[Timer]
|
||||
OnCalendar=
|
||||
OnCalendar={{ autoupdate_schedule }}
|
||||
AccuracySec=1s
|
||||
mode: "0644"
|
||||
notify: Reload systemd
|
||||
|
||||
handlers:
|
||||
- name: Reload systemd
|
||||
systemd:
|
||||
daemon_reload: true
|
||||
|
||||
- name: Restart podman services
|
||||
command: systemctl daemon-reload
|
||||
notify: Reload user systemd
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[[registry]]
|
||||
location = "{{ registry_host }}:{{ registry_port }}"
|
||||
location = "{{ registry_host }}"
|
||||
insecure = false
|
||||
|
||||
Reference in New Issue
Block a user