initial commit

This commit is contained in:
2026-06-30 13:51:06 -04:00
commit d529284141
10 changed files with 439 additions and 0 deletions

70
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,70 @@
pipeline {
agent any
parameters {
string(
name: 'LIMIT',
defaultValue: '',
description: 'Target host(s)/group(s), e.g. "vms", "lxcs", "docker_hosts", "kubernetes", "update_targets", or a hostname (no .lan required). Leave blank to run against all machines.'
)
booleanParam(
name: 'DRY_RUN',
defaultValue: false,
description: 'Run in check mode (no changes made)'
)
}
environment {
PROXMOX_URL = 'https://192.168.0.166:8006'
PROXMOX_USER = 'dynamic-inventory@pve'
PROXMOX_TOKEN_ID = 'dynamic-inventory'
PROXMOX_TOKEN_SECRET = credentials('PROXMOX_TOKEN_SECRET')
ANSIBLE_HOST_KEY_CHECKING = 'False'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Verify Inventory') {
steps {
sh '''
echo "Testing dynamic inventory connection..."
ansible-inventory -i inventories/inventory.proxmox.yml --list | head -10
'''
}
}
stage('Run Playbook') {
steps {
script {
def limitFlag = params.LIMIT ? "--limit '${params.LIMIT}'" : ''
def checkFlag = params.DRY_RUN ? '--check --diff' : ''
echo "Limit: '${params.LIMIT ?: 'all'}'"
echo "Dry run: ${params.DRY_RUN}"
sh """
ansible-playbook \
-i inventories/inventory.proxmox.yml \
site.yml \
${limitFlag} \
${checkFlag}
"""
}
}
}
}
post {
success {
echo "site.yml completed successfully (limit: ${params.LIMIT ?: 'all'})"
}
failure {
echo "site.yml failed (limit: ${params.LIMIT ?: 'all'})"
}
}
}

91
README.md Normal file
View File

@@ -0,0 +1,91 @@
# container_isolation Ansible role
Creates per-app service accounts (UID/GID 2000+) on every host, so each
Docker container can run as its own dedicated UID instead of root or a
shared user. `jerick` is added as a secondary member of every app
group (and the shared `mediapipeline` group), so jerick retains access
everywhere regardless of whatever folder permissions you set up
separately on the NFS server.
**Scope:** this role only manages accounts (groups + users). NFS
folder creation, ownership, and permission bits are handled outside
Ansible, by you, on the NFS server.
## Layout
```
ansible/
├── site.yml # top-level playbook, hosts: all
└── roles/container_isolation/
├── tasks/
│ ├── main.yml # entry point, includes the others
│ ├── sync_users.yml # creates groups + users on every host
│ └── jerick_access.yml # adds jerick to every group, every host
├── vars/main.yml # source of truth: app list, uids, shared groups
└── files/
├── passwd.containers # reference copy, human-readable
└── group.containers # reference copy, human-readable
```
`vars/main.yml` is what Ansible actually reads. The `files/*.containers`
files are a human-readable reference / diff-friendly changelog — not
consumed directly by any task, since the built-in `group`/`user`
modules create accounts idempotently rather than appending raw lines
to `/etc/passwd`.
## Before running
1. Point this at your dynamic inventory however you normally do, e.g.:
```bash
ansible-playbook -i your_dynamic_inventory.py site.yml
```
No inventory group targeting is needed — `site.yml` runs against
`hosts: all`, and the role applies the same logic everywhere.
2. Confirm `jerick` already exists as a real user on every host. The
role does **not** create jerick's account (treated as a
pre-existing human user) — it only adds jerick to the app groups.
If jerick is missing on a host, the role prints a warning and skips
the group-membership tasks for that host rather than failing the
whole run.
## Running
```bash
ansible-playbook -i your_dynamic_inventory.py site.yml --check --diff # dry run first
ansible-playbook -i your_dynamic_inventory.py site.yml
```
Safe to re-run any time — every task uses Ansible's built-in
idempotent modules (`group`, `user`), so a second run produces no
changes.
## Adding a new app later
1. Add a line to `roles/container_isolation/vars/main.yml` under
`container_apps`, with the next free UID (currently up to 2026 —
use 2027 next). **Never reuse or renumber an existing UID** once
real folders have been chowned to it on the NFS server.
2. If it needs shared access to another app's data, add it to the
relevant `shared_groups[].members` list (or create a new shared
group, GID 3001+).
3. Update `files/passwd.containers` and `files/group.containers` to
match, for the human-readable record.
4. Re-run the playbook.
## What you still need to do yourself
- **NFS folder creation/ownership/permissions** on the NFS server —
`chown <uid>:<gid>` and `chmod` each app's folder using the UIDs/GIDs
from `vars/main.yml`. With jerick in every app's group, `770`
(owner+group rw, others none) gives jerick read-write access
everywhere while keeping apps isolated from each other; `750` would
make jerick read-only.
- **`/etc/exports`** on the NFS server — not touched by this role.
- **`docker-compose.yml` edits** — add `user: "UID:GID"` (and
`group_add:` for apps in `mediapipeline`) to each service, using the
UIDs from `vars/main.yml`. Happy to generate these once you share
your compose files.
- **NFS protocol version / idmapping verification** — confirm with
`nfsstat -m` on a client before relying on numeric UID/GID matching
across hosts; NFSv4 idmapping misconfiguration is the most common
cause of permissions silently not working as expected.

View File

@@ -0,0 +1,25 @@
# Proxmox Dynamic Inventory
# Requires PROXMOX_TOKEN_SECRET environment variable to be set
plugin: community.proxmox.proxmox
url: https://192.168.0.166:8006
user: dynamic-inventory@pve
token_id: dynamic-inventory
validate_certs: false
want_facts: true
# Filter to only running machines
filters:
- proxmox_status == 'running'
# Group by Proxmox tags and type
groups:
vms: "'qemu' in proxmox_type"
lxcs: "'lxc' in proxmox_type"
# Tag-based groups - add tags in Proxmox to auto-group
update_targets: "'update' in (proxmox_tags | default([]))"
docker_hosts: "'docker' in (proxmox_tags | default([]))"
kubernetes: "'k8s' in (proxmox_tags | default([]))"
compose:
ansible_host: name + '.lan'
ansible_user: 'jenkins'

View File

@@ -0,0 +1,45 @@
# /etc/group.containers
# Source of truth for container service groups. Sync to all Docker VMs
# and the NFS server using sync-container-users.sh. Append-only — add new
# lines here when adding containers, never renumber existing ones.
#
# Primary per-app groups mirror passwd.containers UIDs (2000-2099).
# Shared groups start at 3000 for cross-container access.
# format: name:x:GID:secondary,members
2fauth:x:2000:
audiobookshelf:x:2001:
authelia:x:2002:
bazarr:x:2003:
cronjobs:x:2004:
dashy:x:2005:
dingus-archiver:x:2006:
finance-app:x:2007:
firefly3:x:2008:
gitea:x:2009:
gotify:x:2010:
immich:x:2011:
it-tools:x:2012:
jellyfin:x:2013:
joplin:x:2014:
kiwix:x:2015:
music-orchestrator:x:2016:
nginxproxy:x:2017:
ombi:x:2018:
picoshare:x:2019:
prowlarr:x:2020:
qbittorrent:x:2021:
radarr:x:2022:
romm:x:2023:
sonarr:x:2024:
tandoor_recipes:x:2025:
vault:x:2026:
#
# Shared groups (cross-container access, GID range 3000+)
# mediapipeline: *arr apps + qbittorrent + jellyfin share the media/download
# folders (e.g. /var/NFSFolder/AppData/media). Ombi is request-only (talks
# to APIs, doesn't touch the filesystem) so it's excluded.
mediapipeline:x:3000:bazarr,prowlarr,qbittorrent,radarr,sonarr,jellyfin
#
# finance-app and dingus-archiver are isolated custom apps with no shared
# access — their primary groups above (2007, 2006) already cover them.
# No secondary group needed unless that changes.

View File

@@ -0,0 +1,34 @@
# /etc/passwd.containers
# Source of truth for container service accounts. Sync to all Docker VMs
# and the NFS server using sync-container-users.sh. Append-only — add new
# lines here when adding containers, never renumber existing ones.
#
# UID range reserved: 2000-2099 (container service accounts)
# format: name:x:UID:GID::/nonexistent:/usr/sbin/nologin
2fauth:x:2000:2000::/nonexistent:/usr/sbin/nologin
audiobookshelf:x:2001:2001::/nonexistent:/usr/sbin/nologin
authelia:x:2002:2002::/nonexistent:/usr/sbin/nologin
bazarr:x:2003:2003::/nonexistent:/usr/sbin/nologin
cronjobs:x:2004:2004::/nonexistent:/usr/sbin/nologin
dashy:x:2005:2005::/nonexistent:/usr/sbin/nologin
dingus-archiver:x:2006:2006::/nonexistent:/usr/sbin/nologin
finance-app:x:2007:2007::/nonexistent:/usr/sbin/nologin
firefly3:x:2008:2008::/nonexistent:/usr/sbin/nologin
gitea:x:2009:2009::/nonexistent:/usr/sbin/nologin
gotify:x:2010:2010::/nonexistent:/usr/sbin/nologin
immich:x:2011:2011::/nonexistent:/usr/sbin/nologin
it-tools:x:2012:2012::/nonexistent:/usr/sbin/nologin
jellyfin:x:2013:2013::/nonexistent:/usr/sbin/nologin
joplin:x:2014:2014::/nonexistent:/usr/sbin/nologin
kiwix:x:2015:2015::/nonexistent:/usr/sbin/nologin
music-orchestrator:x:2016:2016::/nonexistent:/usr/sbin/nologin
nginxproxy:x:2017:2017::/nonexistent:/usr/sbin/nologin
ombi:x:2018:2018::/nonexistent:/usr/sbin/nologin
picoshare:x:2019:2019::/nonexistent:/usr/sbin/nologin
prowlarr:x:2020:2020::/nonexistent:/usr/sbin/nologin
qbittorrent:x:2021:2021::/nonexistent:/usr/sbin/nologin
radarr:x:2022:2022::/nonexistent:/usr/sbin/nologin
romm:x:2023:2023::/nonexistent:/usr/sbin/nologin
sonarr:x:2024:2024::/nonexistent:/usr/sbin/nologin
tandoor_recipes:x:2025:2025::/nonexistent:/usr/sbin/nologin
vault:x:2026:2026::/nonexistent:/usr/sbin/nologin

View File

@@ -0,0 +1,45 @@
---
# roles/container_isolation/tasks/jerick_access.yml
#
# jerick needs read-write access across every app folder for admin/
# maintenance purposes, without weakening isolation between apps from
# each other. Achieved by making jerick a secondary member of every
# app's primary group, combined with 770 permissions on app folders
# (see nfs_folders.yml) — owner: full access, group (incl. jerick):
# full access, others: none.
- name: Check whether jerick exists on this host (read-only, does not create the account)
ansible.builtin.getent:
database: passwd
key: "{{ jerick_user }}"
register: jerick_lookup
failed_when: false
- name: Warn if jerick user does not exist on this host
ansible.builtin.debug:
msg: >-
WARNING: user '{{ jerick_user }}' was not found on this host.
This role does not create jerick's account (assumed to be a
pre-existing human user, not a service account). Create it first,
then re-run this role so jerick gets added to the app groups below.
when: jerick_lookup.failed | default(false)
- name: Add jerick to every app's primary group
ansible.builtin.user:
name: "{{ jerick_user }}"
groups: "{{ item.name }}"
append: true
loop: "{{ container_apps }}"
loop_control:
label: "{{ jerick_user }} -> {{ item.name }}"
when: not (jerick_lookup.failed | default(false))
- name: Add jerick to every shared group
ansible.builtin.user:
name: "{{ jerick_user }}"
groups: "{{ item.name }}"
append: true
loop: "{{ shared_groups }}"
loop_control:
label: "{{ jerick_user }} -> {{ item.name }}"
when: not (jerick_lookup.failed | default(false))

View File

@@ -0,0 +1,9 @@
---
# roles/container_isolation/tasks/main.yml
- name: Sync container service groups and users (all hosts)
import_tasks: sync_users.yml
- name: Add jerick to every app and shared group (all hosts)
import_tasks: jerick_access.yml

View File

@@ -0,0 +1,49 @@
---
# roles/container_isolation/tasks/sync_users.yml
#
# Creates each app's primary group + user, and each shared group, using
# Ansible's built-in group/user modules instead of hand-rolled file
# appends — this is idempotent and safe to re-run, and avoids the
# malformed-/etc/passwd risk of manual editing.
- name: Create primary group for each container app
ansible.builtin.group:
name: "{{ item.name }}"
gid: "{{ item.uid }}"
state: present
loop: "{{ container_apps }}"
loop_control:
label: "{{ item.name }} (gid {{ item.uid }})"
- name: Create shared (secondary) groups
ansible.builtin.group:
name: "{{ item.name }}"
gid: "{{ item.gid }}"
state: present
loop: "{{ shared_groups }}"
loop_control:
label: "{{ item.name }} (gid {{ item.gid }})"
- name: Create service user for each container app
ansible.builtin.user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
group: "{{ item.name }}"
shell: /usr/sbin/nologin
home: /nonexistent
create_home: false
system: true
state: present
loop: "{{ container_apps }}"
loop_control:
label: "{{ item.name }} (uid {{ item.uid }})"
- name: Add app users to their shared groups
ansible.builtin.user:
name: "{{ item.0.name }}"
groups: "{{ item.1.name }}"
append: true
loop: "{{ container_apps | product(shared_groups) | list }}"
loop_control:
label: "{{ item.0.name }} -> {{ item.1.name }}"
when: item.0.name in item.1.members

View File

@@ -0,0 +1,58 @@
---
# roles/container_isolation/vars/main.yml
#
# Single source of truth for container service accounts. Mirrors
# files/passwd.containers and files/group.containers — keep in sync if
# you edit one, edit the other. Append-only: never renumber existing
# UIDs/GIDs once folders have been chowned on the NFS server.
jerick_user: jerick
# Every container service account. uid == primary gid for each.
# jerick is added as a secondary member of every app's primary group
# in the group sync task, giving him rw access (770) without weakening
# isolation between apps.
container_apps:
- { name: "2fauth", uid: 2000 }
- { name: "audiobookshelf", uid: 2001 }
- { name: "authelia", uid: 2002 }
- { name: "bazarr", uid: 2003 }
- { name: "cronjobs", uid: 2004 }
- { name: "dashy", uid: 2005 }
- { name: "dingus-archiver", uid: 2006 }
- { name: "finance-app", uid: 2007 }
- { name: "firefly3", uid: 2008 }
- { name: "gitea", uid: 2009 }
- { name: "gotify", uid: 2010 }
- { name: "immich", uid: 2011 }
- { name: "it-tools", uid: 2012 }
- { name: "jellyfin", uid: 2013 }
- { name: "joplin", uid: 2014 }
- { name: "kiwix", uid: 2015 }
- { name: "music-orchestrator", uid: 2016 }
- { name: "nginxproxy", uid: 2017 }
- { name: "ombi", uid: 2018 }
- { name: "picoshare", uid: 2019 }
- { name: "prowlarr", uid: 2020 }
- { name: "qbittorrent", uid: 2021 }
- { name: "radarr", uid: 2022 }
- { name: "romm", uid: 2023 }
- { name: "sonarr", uid: 2024 }
- { name: "tandoor_recipes", uid: 2025 }
- { name: "vault", uid: 2026 }
# Shared secondary groups for cross-container access. "members" lists
# app names from container_apps that need to be added to this group
# IN ADDITION to their own primary group. jerick is added automatically
# to every shared group as well as every per-app primary group — see
# tasks/sync_users.yml.
shared_groups:
- name: mediapipeline
gid: 3000
members:
- bazarr
- prowlarr
- qbittorrent
- radarr
- sonarr
- jellyfin

13
site.yml Normal file
View File

@@ -0,0 +1,13 @@
---
# site.yml
#
# Applies the container_isolation role to every host in inventory.
# Scope: service accounts only (groups + users + jerick's group
# memberships). Run against your dynamic inventory as-is — no group
# targeting needed since every host gets the same treatment.
- name: Configure container service accounts
hosts: all
become: true
roles:
- container_isolation