From 61218cb597504cadb7513648c2b095916d0ed691 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 1 Oct 2026 08:12:09 +0000 Subject: [PATCH 1/7] feat: install koncreet on every apply, with an Ubuntu 26.04 patch layered on Every apply resolves the latest jimididit/koncreet release, verifies koncreet.tar.gz against the GitHub release-asset digest, extracts it as root into /usr/local/lib/code-factory/koncreet/-/ and links /usr/local/bin/koncreet. Nothing runs it. patches/koncreet/ubuntu-26.04.patch opens the OS gate and doctor for Ubuntu 26.04 and restores the last SSH client IP fallback there (26.04 keeps no utmp, so who -m prints nothing; ask logind instead). The same change is the ubuntu-26.04 branch of undeemed/koncreet. Apply layers the patch on each new release and reports applied, skipped because upstream already supports 26.04, or skipped because it no longer applies; the patch never fails the apply. /etc/koncreet.conf is rendered once for the fleet: the operator account as the sudo user with its own authorized_keys, SSH kept open, 41641/udp for Tailscale. docs/security.md has the one-time manual run, the tailscale0 ufw rule, and upstream's lockout recovery. --- ansible/group_vars/all.yml | 15 +++- ansible/site.yml | 4 ++ ansible/tasks/koncreet.yml | 104 ++++++++++++++++++++++++++++ ansible/tasks/preflight.yml | 2 +- ansible/templates/koncreet.conf.j2 | 26 +++++++ docs/dependencies.md | 8 ++- docs/security.md | 30 ++++++++ patches/koncreet/ubuntu-26.04.patch | 68 ++++++++++++++++++ scripts/install_tools.py | 4 +- tests/test_install_tools.py | 5 +- 10 files changed, 259 insertions(+), 7 deletions(-) create mode 100644 ansible/tasks/koncreet.yml create mode 100644 ansible/templates/koncreet.conf.j2 create mode 100644 patches/koncreet/ubuntu-26.04.patch diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index cb2c8d6..585abca 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -187,8 +187,9 @@ factory_installer_selection: >- + (['--npm'] if (factory_cfg.profiles.agents | bool) else []) + (['--development'] if (factory_cfg.profiles.development | bool) else []) }} factory_installer_also: >- - {{ (['psutil'] if ((factory_cfg.profiles.agents | bool) - and (factory_cfg.browser_prune.enabled | bool)) else []) + {{ ['koncreet'] + + (['psutil'] if ((factory_cfg.profiles.agents | bool) + and (factory_cfg.browser_prune.enabled | bool)) else []) + (['obscura', 'supabase'] if (factory_cfg.profiles.fleet_guards | bool) else []) }} # --- Herdr static config ---------------------------------------------------- @@ -361,6 +362,16 @@ factory_tailscale_keyring: /usr/share/keyrings/tailscale-archive-keyring.gpg # package from the stable track. factory_tailscale_version: "" +# --- Koncreet (install only; the operator runs it by hand) ------------------ +# The latest jimididit/koncreet release, verified against the SHA-256 GitHub +# publishes for the asset, with Code Factory's Ubuntu 26.04 patch layered on. +# Root runs it, so the release and its configuration are root-owned, never in +# the account's home. docs/security.md has the manual run. +factory_koncreet: "{{ factory_latest.koncreet.assets[factory_platform] }}" +factory_koncreet_patch: "{{ code_factory_repo }}/patches/koncreet/ubuntu-26.04.patch" +factory_koncreet_prefix: /usr/local/lib/code-factory/koncreet +factory_koncreet_config: /etc/koncreet.conf + # --- Chrome ----------------------------------------------------------------- # auto -> install only when the desktop profile is enabled # true -> always install (headless AXI bridge use without a desktop) diff --git a/ansible/site.yml b/ansible/site.yml index d1c5c26..8e8ae17 100644 --- a/ansible/site.yml +++ b/ansible/site.yml @@ -110,6 +110,10 @@ when: factory_cfg.profiles.tailscale | bool tags: [tailscale] + - name: Koncreet host hardening toolkit, installed but never run + ansible.builtin.import_tasks: tasks/koncreet.yml + tags: [koncreet] + - name: Google Chrome from the official distribution repository ansible.builtin.import_tasks: tasks/browser.yml when: >- diff --git a/ansible/tasks/koncreet.yml b/ansible/tasks/koncreet.yml new file mode 100644 index 0000000..6ea2175 --- /dev/null +++ b/ansible/tasks/koncreet.yml @@ -0,0 +1,104 @@ +--- +# Koncreet host hardening: installed on every apply, never run. The operator +# runs it once, by hand (docs/security.md, Host hardening). Root runs it, so +# the release and its configuration are root-owned and outside the account's +# home, where an agent could rewrite what root later executes. + +- name: Hash the Ubuntu 26.04 koncreet patch + ansible.builtin.stat: + path: "{{ factory_koncreet_patch }}" + checksum_algorithm: sha256 + register: factory_koncreet_patch_stat + check_mode: false + +# A new release or a changed patch lands in its own directory, so an unchanged +# pair is a no-op and /usr/local/bin/koncreet only moves once the new tree is +# complete. patch-outcome is written last and marks the tree complete. +- name: Name the koncreet release directory + ansible.builtin.set_fact: + factory_koncreet_dir: >- + {{ factory_koncreet_prefix }}/{{ factory_latest.koncreet.version + }}-{{ factory_koncreet_patch_stat.stat.checksum[:12] }} + +- name: Stat the installed koncreet release + ansible.builtin.stat: + path: "{{ factory_koncreet_dir }}/patch-outcome" + register: factory_koncreet_stat + check_mode: false + +- name: Install the latest koncreet release with the Ubuntu 26.04 patch + when: + - not factory_koncreet_stat.stat.exists + - not ansible_check_mode + become: true + block: + - name: Create the koncreet release directory + ansible.builtin.file: + path: "{{ factory_koncreet_dir }}" + state: directory + owner: root + group: root + mode: "0755" + + - name: Fetch the koncreet tarball + ansible.builtin.get_url: + url: "{{ factory_koncreet.url }}" + dest: "{{ factory_koncreet_dir }}.tar.gz" + checksum: "sha256:{{ factory_koncreet.sha256 }}" + mode: "0600" + + - name: Extract koncreet + ansible.builtin.unarchive: + src: "{{ factory_koncreet_dir }}.tar.gz" + dest: "{{ factory_koncreet_dir }}" + remote_src: true + owner: root + group: root + + - name: Remove the koncreet tarball + ansible.builtin.file: + path: "{{ factory_koncreet_dir }}.tar.gz" + state: absent + + # The patch never fails the apply: when upstream already supports 26.04 or + # the patch no longer applies, koncreet installs as released and the + # outcome says which case this release hit. + - name: Layer the Ubuntu 26.04 patch on the release + ansible.builtin.shell: | + cd koncreet || exit 1 + if grep -q '26\.04' lib/os.sh; then + echo "patch skipped: koncreet {{ factory_latest.koncreet.version }} already supports Ubuntu 26.04" + elif git apply {{ factory_koncreet_patch | quote }} 2>/dev/null; then + echo "patch applied: Ubuntu 26.04 support on koncreet {{ factory_latest.koncreet.version }}" + else + echo "patch skipped: it no longer applies to koncreet {{ factory_latest.koncreet.version }}, installed as released" + fi | tee ../patch-outcome.new && mv ../patch-outcome.new ../patch-outcome + args: + chdir: "{{ factory_koncreet_dir }}" + environment: + # Outside any repository, so git apply patches files like patch(1). + GIT_CEILING_DIRECTORIES: "{{ factory_koncreet_dir }}" + register: factory_koncreet_outcome + changed_when: true + + - name: Report the koncreet release and patch outcome + ansible.builtin.debug: + msg: "koncreet {{ factory_latest.koncreet.version }}: {{ factory_koncreet_outcome.stdout }}" + +- name: Link koncreet into /usr/local/bin + ansible.builtin.file: + src: "{{ factory_koncreet_dir }}/koncreet/koncreet" + dest: /usr/local/bin/koncreet + state: link + become: true + when: not ansible_check_mode + +- name: Render the fleet koncreet configuration once + ansible.builtin.template: + src: koncreet.conf.j2 + dest: "{{ factory_koncreet_config }}" + owner: root + group: root + mode: "0644" + force: false + become: true diff --git a/ansible/tasks/preflight.yml b/ansible/tasks/preflight.yml index 2e5f66e..6bdd4db 100644 --- a/ansible/tasks/preflight.yml +++ b/ansible/tasks/preflight.yml @@ -47,7 +47,7 @@ - name: Build the list of Main-owned inputs this run requires ansible.builtin.set_fact: factory_required_inputs: >- - {{ [factory_installer, factory_herdr_spaces_source] + {{ [factory_installer, factory_herdr_spaces_source, factory_koncreet_patch] + (factory_agent_config_files | map(attribute='src') | map('regex_replace', '^', code_factory_repo ~ '/') diff --git a/ansible/templates/koncreet.conf.j2 b/ansible/templates/koncreet.conf.j2 new file mode 100644 index 0000000..bcd552a --- /dev/null +++ b/ansible/templates/koncreet.conf.j2 @@ -0,0 +1,26 @@ +# Koncreet configuration for this host, rendered once by Code Factory +# (ansible/templates/koncreet.conf.j2). Edit it here: apply never overwrites it. +# Nothing runs koncreet automatically. Run it by hand, in this order; the full +# steps and the lockout recovery are in docs/security.md (Host hardening): +# sudo ufw allow in on tailscale0 +# sudo koncreet doctor +# sudo koncreet --dry-run apply -c {{ factory_koncreet_config }} +# sudo koncreet apply -c {{ factory_koncreet_config }} +# sudo koncreet ssh apply only once {{ ansible_user_id }} can open a new SSH session and run sudo true + +# SSH hardening is the separate last step above, so it is not listed here. +modules=baseline,firewall,fail2ban,updates + +# The account that ran ./factory apply keeps the SSH keys it logs in with and +# is made a sudo user. +user={{ ansible_user_id }} +pubkey_file={{ ansible_user_dir }}/.ssh/authorized_keys + +# SSH stays open: koncreet always allows the ports sshd listens on. 41641/udp +# is Tailscale's direct WireGuard port. Tailnet traffic arrives on tailscale0, +# which this file cannot name: run sudo ufw allow in on tailscale0 first. +firewall_ports=41641/udp +firewall_public=false + +fail2ban_services=ssh +auto_reboot=false diff --git a/docs/dependencies.md b/docs/dependencies.md index eaf1f00..f977f38 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -23,7 +23,7 @@ Everything the recipe installs, grouped by where it comes from. Nothing is pinne - `agents` profile, omp plugins: ponytail ([DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail)), i-have-adhd ([ayghri/i-have-adhd](https://github.com/ayghri/i-have-adhd)) and caveman ([JuliusBrussee/caveman](https://github.com/JuliusBrussee/caveman)) from their GitHub marketplaces, installed once and upgraded with `omp plugin upgrade` on every apply. These are the only installs that are not checksum-verified: no publisher posts a checksum for them, so they track each author's default branch and load as agent instructions and hooks. The operator accepted this to keep them at the latest commit. - `development` profile: rustup-init, the version in rustup's [stable release](https://static.rust-lang.org/rustup/release-stable.toml), verified against the `.sha256` published beside it, installing the Rust `stable` toolchain (minimal profile + rustfmt + clippy). Every apply moves the toolchain to the newest stable. -The GitHub lookups use the GitHub API, which allows 60 unauthenticated requests an hour per IP (shared IPs such as CI runners exhaust it); a resolution makes one request per GitHub-hosted tool the host installs, at most seven. Only the tools a run installs are resolved, so a source the host does not use cannot fail it. The lookups authenticate with `GITHUB_TOKEN` from the environment that runs `./factory apply` or `./bootstrap.sh`, else run unauthenticated; the token is sent to the GitHub API only. Container builds take the token as the optional BuildKit secret `github_token` (`docker build --secret id=github_token,env=GITHUB_TOKEN ...`), so it never lands in the image. +The GitHub lookups use the GitHub API, which allows 60 unauthenticated requests an hour per IP (shared IPs such as CI runners exhaust it); a resolution makes one request per GitHub-hosted tool the host installs, at most eight. Only the tools a run installs are resolved, so a source the host does not use cannot fail it. The lookups authenticate with `GITHUB_TOKEN` from the environment that runs `./factory apply` or `./bootstrap.sh`, else run unauthenticated; the token is sent to the GitHub API only. Container builds take the token as the optional BuildKit secret `github_token` (`docker build --secret id=github_token,env=GITHUB_TOKEN ...`), so it never lands in the image. ## Ubuntu packages @@ -43,6 +43,12 @@ The GitHub lookups use the GitHub API, which allows 60 unauthenticated requests - Obscura, the latest [h4ckf0r0day/obscura release](https://github.com/h4ckf0r0day/obscura/releases/latest) for the host's platform, verified against the GitHub release-asset digest (`ansible/tasks/fleet-browsers.yml`). Each release extracts into its own `~/oss-fleet/browsers/obscura-/`. - Supabase CLI, the npm registry's latest `supabase`, installed with `npm install` into `~/oss-fleet/shared-supabase` (`ansible/tasks/fleet_guards.yml`). +## Koncreet + +Every host. + +- [Koncreet](https://github.com/jimididit/koncreet), the latest release's `koncreet.tar.gz`, verified against the GitHub release-asset digest (`ansible/tasks/koncreet.yml`). It installs as root into `/usr/local/lib/code-factory/koncreet/-/` with `/usr/local/bin/koncreet` linked to it, and `patches/koncreet/ubuntu-26.04.patch` is layered on top. Apply never runs it; [Host hardening](security.md#host-hardening) has the manual run. + ## Chrome autopruner `agents` profile with `browser_prune.enabled`. diff --git a/docs/security.md b/docs/security.md index c0afbc5..4166350 100644 --- a/docs/security.md +++ b/docs/security.md @@ -35,6 +35,36 @@ Tailscale installation, authentication, and SSH authorization are separate steps This export does not rewrite the current host's firewall, SSH policy, account membership, or credentials. Review those changes separately before applying a new-host profile. +## Host hardening + +Every apply installs [Koncreet](https://github.com/jimididit/koncreet) as `/usr/local/bin/koncreet` and renders `/etc/koncreet.conf` once, but nothing runs it. It is a first-hour hardening toolkit: a sudo user with SSH keys, sysctl, swap, a journald cap, time sync, a ufw default-deny firewall, fail2ban on SSH, unattended security updates, and finally SSH with password and root login turned off. + +Upstream supports Debian 12/13 and Ubuntu 22.04/24.04 only. `patches/koncreet/ubuntu-26.04.patch` adds Ubuntu 26.04: it opens the OS gate and doctor, and restores the last fallback Koncreet uses to find your SSH client address for the fail2ban whitelist: 26.04 keeps no utmp, so `who -m` prints nothing, and the patch asks logind instead. The same change is the `ubuntu-26.04` branch of the [undeemed/koncreet](https://github.com/undeemed/koncreet/tree/ubuntu-26.04) fork; regenerate the patch from there with `git diff main...ubuntu-26.04`. Apply layers the patch on each new release and prints which case it hit: applied; skipped because the release already supports 26.04; or skipped because it no longer applies, in which case Koncreet installs as released and refuses to run on 26.04 until the patch is refreshed. The patch never fails the apply. + +`/etc/koncreet.conf` makes the account that ran `./factory apply` the sudo user, installs the SSH keys it logs in with, keeps SSH open (Koncreet always allows the ports sshd listens on) and opens 41641/udp for Tailscale's direct connections. Apply never overwrites it; edit it there. + +Run it once, by hand, from an SSH session you keep open until the last step works: + +1. `sudo ufw allow in on tailscale0`, so the tailnet stays reachable once ufw denies incoming traffic. Koncreet keeps existing ufw rules. +2. `sudo koncreet doctor` +3. `sudo koncreet --dry-run apply -c /etc/koncreet.conf`, and read the plan. +4. `sudo koncreet apply -c /etc/koncreet.conf` +5. Open a new SSH session as that user and run `sudo true`. Only when it works: `sudo koncreet ssh apply`. Test one more new session before you close the first. Not `sudo -v`: once the account is in the `sudo` group, `sudo -v` asks for a password even when sudoers grants it NOPASSWD, and a cloud account has none. + +If something goes wrong (from upstream's README): + +| Problem | Fix | +|---------|-----| +| Can't SSH after harden | `sudo koncreet ssh undo` | +| Locked out by ufw | Console: `sudo ufw disable` | +| Banned by fail2ban | `sudo koncreet fail2ban unban YOUR.IP` | +| Undo baseline drop-ins | `sudo koncreet baseline undo` (keeps users/swap/timezone) | +| Need the new user password | `cat /root/USER.koncreet-password` (as root - save it before `ssh apply`) | +| Forced password change fails | `chage -d $(date -I) USER` then reconnect with your key | +| Too many authentication failures | `ssh -o IdentitiesOnly=yes -i ~/.ssh/your_key user@host` | + +Logs: `/var/log/koncreet.log`. Backups: `*.koncreet.bak`. The provider's console, and `tailscale ssh` where Tailscale SSH is enabled, reach the host when sshd does not. + ## Updates Tools track their latest release, and every download is verified against the checksum its publisher posts for that release (what each source checks is in [Dependencies](dependencies.md)); a release without one is refused. The one exception is the three omp marketplace plugins (ponytail, i-have-adhd, caveman): no publisher checksums them, they track each author's default branch, and they load as agent instructions and hooks. The operator accepted that to keep them at the latest commit. Checksums prove a download is the published artifact; they do not establish that a publisher is trustworthy. Review added tools and installer behavior before adding them. Ubuntu security updates remain an operating-system responsibility rather than freezing an entire vulnerable package index forever. diff --git a/patches/koncreet/ubuntu-26.04.patch b/patches/koncreet/ubuntu-26.04.patch new file mode 100644 index 0000000..7a43835 --- /dev/null +++ b/patches/koncreet/ubuntu-26.04.patch @@ -0,0 +1,68 @@ +diff --git a/lib/doctor.sh b/lib/doctor.sh +index 139246c..5771fe4 100644 +--- a/lib/doctor.sh ++++ b/lib/doctor.sh +@@ -25,7 +25,7 @@ koncreet_os_is_supported() { + ;; + ubuntu) + case "$KONCREET_OS_VERSION" in +- 22.04|24.04) return 0 ;; ++ 22.04|24.04|26.04) return 0 ;; + esac + ;; + esac +@@ -43,7 +43,7 @@ cmd_doctor() { + if koncreet_os_is_supported; then + doctor_ok "OS ${KONCREET_OS_ID} ${KONCREET_OS_VERSION} supported" + else +- doctor_fail "OS ${KONCREET_OS_ID:-unknown} ${KONCREET_OS_VERSION:-} not supported (need Debian 12/13 or Ubuntu 22.04/24.04)" ++ doctor_fail "OS ${KONCREET_OS_ID:-unknown} ${KONCREET_OS_VERSION:-} not supported (need Debian 12/13 or Ubuntu 22.04/24.04/26.04)" + fi + + # --- root (apply readiness) --- +diff --git a/lib/os.sh b/lib/os.sh +index 38254c1..10295f6 100644 +--- a/lib/os.sh ++++ b/lib/os.sh +@@ -56,13 +56,13 @@ koncreet_require_supported_os() { + ;; + ubuntu) + case "$KONCREET_OS_VERSION" in +- 22.04|24.04) ok=1 ;; ++ 22.04|24.04|26.04) ok=1 ;; + esac + ;; + esac + if [[ "$ok" -ne 1 ]]; then + log_error "Unsupported OS: ${KONCREET_OS_ID:-unknown} ${KONCREET_OS_VERSION:-}" +- log_error "Koncreet supports Debian 12/13 and Ubuntu 22.04/24.04 only." ++ log_error "Koncreet supports Debian 12/13 and Ubuntu 22.04/24.04/26.04 only." + log_error "Refuse rather than half-apply on ${KONCREET_OS_ID:-unknown}." + exit 2 + fi +diff --git a/lib/sshd.sh b/lib/sshd.sh +index 6686f4a..3a32bbb 100644 +--- a/lib/sshd.sh ++++ b/lib/sshd.sh +@@ -95,7 +95,8 @@ koncreet_valid_ip() { + + # Print the IP of the SSH client that started this session; returns 1 if unknown. + # sudo strips SSH_CONNECTION, so fall back to the environment of our ancestor +-# processes (the login shell still has it), then to who -m. ++# processes (the login shell still has it), then to logind's record of the login ++# session, then to who -m. + koncreet_ssh_client_ip() { + local ip pid="$$" var + ip="${SSH_CONNECTION:-${SSH_CLIENT:-}}" +@@ -109,6 +110,11 @@ koncreet_ssh_client_ip() { + pid="$(awk '/^PPid:/{print $2}' "/proc/$pid/status" 2>/dev/null || true)" + pid="${pid:-0}" + done ++ # Ubuntu 26.04 keeps no utmp, so who -m prints nothing there, but logind still ++ # records the remote host of the login session we run in. ++ if [[ -z "$ip" ]] && command -v loginctl &>/dev/null; then ++ ip="$(loginctl show-session self -p RemoteHost --value 2>/dev/null || true)" ++ fi + if [[ -z "$ip" ]]; then + ip="$(who -m 2>/dev/null | sed -n 's/.*(\(.*\)).*/\1/p' || true)" + fi diff --git a/scripts/install_tools.py b/scripts/install_tools.py index 047a53d..1046d59 100755 --- a/scripts/install_tools.py +++ b/scripts/install_tools.py @@ -70,6 +70,8 @@ "obscura-{gnu}-linux.tar.gz", {"obscura": "obscura", "obscura-worker": "obscura-worker"}, ), + # Resolved for host hardening (ansible/tasks/koncreet.yml), installed there as root. + "koncreet": ("jimididit/koncreet", "v", "koncreet.tar.gz", {"koncreet": "koncreet/koncreet"}), } # npm tools on the registry's latest version, each installed into its own prefix. NPM_LATEST = { @@ -509,7 +511,7 @@ def main(): "--also", default="", help="comma-separated extra sources to resolve that Ansible installs itself: " - "obscura, supabase, psutil", + "obscura, supabase, psutil, koncreet", ) parser.add_argument( "--resolve", diff --git a/tests/test_install_tools.py b/tests/test_install_tools.py index 638d1b5..979922b 100644 --- a/tests/test_install_tools.py +++ b/tests/test_install_tools.py @@ -106,6 +106,7 @@ def test_download_rejects_plain_http(tmp_path): "kunchenguid/treehouse": ("v9.9.9", "treehouse-v9.9.9-linux-amd64.tar.gz"), "astral-sh/uv": ("9.9.9", "uv-x86_64-unknown-linux-gnu.tar.gz"), "h4ckf0r0day/obscura": ("v9.9.9", "obscura-x86_64-linux.tar.gz"), + "jimididit/koncreet": ("v9.9.9", "koncreet.tar.gz"), } @@ -179,9 +180,9 @@ def release(tag, name, draft=False): def test_latest_releases_are_pinned_to_the_digests_their_publishers_list(monkeypatch): upstream(monkeypatch) latest = installer.resolve_latest("linux-x86_64", EVERYTHING) - for tool in ("herdr", "bun", "gh", "no-mistakes", "treehouse", "uv", "obscura"): + for tool in ("herdr", "bun", "gh", "no-mistakes", "treehouse", "uv", "obscura", "koncreet"): assert latest[tool]["assets"]["linux-x86_64"]["sha256"] == "a" * 64 - for tool in ("herdr", "bun", "gh", "treehouse", "uv", "obscura"): + for tool in ("herdr", "bun", "gh", "treehouse", "uv", "obscura", "koncreet"): assert latest[tool]["version"] == "9.9.9" assert latest["gh"]["assets"]["linux-x86_64"]["format"] == "tar" assert latest["bun"]["assets"]["linux-x86_64"]["format"] == "zip" From bbc9a85e1c5804310c1a22264f922f1c79a138f4 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 1 Oct 2026 08:13:19 +0000 Subject: [PATCH 2/7] fix(koncreet): never render root as the koncreet sudo user A root-run apply now leaves user= and pubkey_file= commented for the operator to fill in, and the template says why the operator login, not factory.user, is the sudo user. --- ansible/templates/koncreet.conf.j2 | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/ansible/templates/koncreet.conf.j2 b/ansible/templates/koncreet.conf.j2 index bcd552a..7fbc8bf 100644 --- a/ansible/templates/koncreet.conf.j2 +++ b/ansible/templates/koncreet.conf.j2 @@ -6,15 +6,22 @@ # sudo koncreet doctor # sudo koncreet --dry-run apply -c {{ factory_koncreet_config }} # sudo koncreet apply -c {{ factory_koncreet_config }} -# sudo koncreet ssh apply only once {{ ansible_user_id }} can open a new SSH session and run sudo true +# sudo koncreet ssh apply only once that user can open a new SSH session and run sudo true # SSH hardening is the separate last step above, so it is not listed here. modules=baseline,firewall,fail2ban,updates -# The account that ran ./factory apply keeps the SSH keys it logs in with and -# is made a sudo user. +# The account that ran ./factory apply is the operator: it keeps the SSH keys +# it logs in with and is made a sudo user. Not factory.user, which runs the +# agents and must not gain sudo through this file. +{% if ansible_user_id != 'root' %} user={{ ansible_user_id }} pubkey_file={{ ansible_user_dir }}/.ssh/authorized_keys +{% else %} +# Apply ran as root, so the operator account is unknown. Set both before use: +# user= +# pubkey_file=/home//.ssh/authorized_keys +{% endif %} # SSH stays open: koncreet always allows the ports sshd listens on. 41641/udp # is Tailscale's direct WireGuard port. Tailnet traffic arrives on tailscale0, From 69d49a5dd31a0ceefd183aa624520f210463ea18 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 1 Oct 2026 08:28:35 +0000 Subject: [PATCH 3/7] no-mistakes(review): Validate release version; drop it from root shell --- ansible/tasks/koncreet.yml | 6 +++--- scripts/install_tools.py | 2 ++ tests/test_install_tools.py | 8 ++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/ansible/tasks/koncreet.yml b/ansible/tasks/koncreet.yml index 6ea2175..6d8d580 100644 --- a/ansible/tasks/koncreet.yml +++ b/ansible/tasks/koncreet.yml @@ -67,11 +67,11 @@ ansible.builtin.shell: | cd koncreet || exit 1 if grep -q '26\.04' lib/os.sh; then - echo "patch skipped: koncreet {{ factory_latest.koncreet.version }} already supports Ubuntu 26.04" + echo "patch skipped: this release already supports Ubuntu 26.04" elif git apply {{ factory_koncreet_patch | quote }} 2>/dev/null; then - echo "patch applied: Ubuntu 26.04 support on koncreet {{ factory_latest.koncreet.version }}" + echo "patch applied: Ubuntu 26.04 support layered on this release" else - echo "patch skipped: it no longer applies to koncreet {{ factory_latest.koncreet.version }}, installed as released" + echo "patch skipped: it no longer applies, installed as released" fi | tee ../patch-outcome.new && mv ../patch-outcome.new ../patch-outcome args: chdir: "{{ factory_koncreet_dir }}" diff --git a/scripts/install_tools.py b/scripts/install_tools.py index 1046d59..9a37257 100755 --- a/scripts/install_tools.py +++ b/scripts/install_tools.py @@ -127,6 +127,8 @@ def fetch(url, what): def verified(tool, version, key, name, url, checksum, binaries): """A lock-shaped spec, only when the publisher lists a SHA-256 for the asset.""" + if not re.fullmatch(r"[A-Za-z0-9_.-]+", version): + raise ValueError(f"{tool} release tag {version!r} is not a safe version; refusing it") if not re.fullmatch(r"[0-9a-f]{64}", checksum): raise ValueError( f"{tool} {version} publishes no SHA-256 for {name}; refusing an unverified binary" diff --git a/tests/test_install_tools.py b/tests/test_install_tools.py index 979922b..e2858ce 100644 --- a/tests/test_install_tools.py +++ b/tests/test_install_tools.py @@ -233,6 +233,14 @@ def test_release_without_a_published_checksum_is_refused(monkeypatch, source): installer.resolve_latest("linux-x86_64", EVERYTHING) +@pytest.mark.parametrize("tag", ["v1.0$(id)", "v1;id", 'v1"x', "v1`id`", "v1|id", "v1 2", "v/1"]) +def test_release_tag_that_is_not_a_plain_version_is_refused(monkeypatch, tag): + monkeypatch.setitem(RELEASES, "jimididit/koncreet", (tag, "koncreet.tar.gz")) + upstream(monkeypatch) + with pytest.raises(ValueError, match="not a safe version"): + installer.resolve_latest("linux-x86_64", EVERYTHING) + + @pytest.mark.parametrize(("env", "expected"), [("env-token", "Bearer env-token"), ("", None)]) def test_github_token_goes_only_to_the_github_api(monkeypatch, env, expected): seen = [] From a401b6b5b2916e96550d74e180b796e812ede3cd Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 1 Oct 2026 08:56:17 +0000 Subject: [PATCH 4/7] no-mistakes(review): Make koncreet optional; guard config for tailscale and factory user --- ansible/group_vars/all.yml | 6 +- ansible/site.yml | 1 + ansible/tasks/koncreet.yml | 198 +++++++++++++++-------------- ansible/tasks/preflight.yml | 5 + ansible/templates/koncreet.conf.j2 | 30 +++-- containers/factory.container.yml | 3 +- docs/dependencies.md | 6 +- docs/security.md | 8 +- scripts/install_tools.py | 43 ++++--- tests/test_configuration.py | 162 +++++++++++++++++++++++ tests/test_install_tools.py | 65 +++++++++- 11 files changed, 397 insertions(+), 130 deletions(-) diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index 585abca..14663da 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -187,7 +187,7 @@ factory_installer_selection: >- + (['--npm'] if (factory_cfg.profiles.agents | bool) else []) + (['--development'] if (factory_cfg.profiles.development | bool) else []) }} factory_installer_also: >- - {{ ['koncreet'] + {{ (['koncreet'] if (factory_cfg.start_services | bool) else []) + (['psutil'] if ((factory_cfg.profiles.agents | bool) and (factory_cfg.browser_prune.enabled | bool)) else []) + (['obscura', 'supabase'] if (factory_cfg.profiles.fleet_guards | bool) else []) }} @@ -366,7 +366,9 @@ factory_tailscale_version: "" # The latest jimididit/koncreet release, verified against the SHA-256 GitHub # publishes for the asset, with Code Factory's Ubuntu 26.04 patch layered on. # Root runs it, so the release and its configuration are root-owned, never in -# the account's home. docs/security.md has the manual run. +# the account's home. docs/security.md has the manual run. Optional: it is +# resolved only when start_services is true (never in the container image), and +# a failed lookup, digest, or download skips it with a warning. factory_koncreet: "{{ factory_latest.koncreet.assets[factory_platform] }}" factory_koncreet_patch: "{{ code_factory_repo }}/patches/koncreet/ubuntu-26.04.patch" factory_koncreet_prefix: /usr/local/lib/code-factory/koncreet diff --git a/ansible/site.yml b/ansible/site.yml index 8e8ae17..d61a402 100644 --- a/ansible/site.yml +++ b/ansible/site.yml @@ -112,6 +112,7 @@ - name: Koncreet host hardening toolkit, installed but never run ansible.builtin.import_tasks: tasks/koncreet.yml + when: "'koncreet' in factory_latest" tags: [koncreet] - name: Google Chrome from the official distribution repository diff --git a/ansible/tasks/koncreet.yml b/ansible/tasks/koncreet.yml index 6d8d580..6bce36c 100644 --- a/ansible/tasks/koncreet.yml +++ b/ansible/tasks/koncreet.yml @@ -1,104 +1,118 @@ --- -# Koncreet host hardening: installed on every apply, never run. The operator -# runs it once, by hand (docs/security.md, Host hardening). Root runs it, so -# the release and its configuration are root-owned and outside the account's -# home, where an agent could rewrite what root later executes. +# Koncreet host hardening: installed on every apply that starts services +# (never in the container image), never run. The operator runs it once, by +# hand (docs/security.md, Host hardening). Root runs it, so the release and +# its configuration are root-owned and outside the account's home, where an +# agent could rewrite what root later executes. +# +# It is optional: any failure here, a digest mismatch included, skips koncreet +# with a warning and leaves the rest of the apply running. A tree that failed +# its digest check is never installed or linked. -- name: Hash the Ubuntu 26.04 koncreet patch - ansible.builtin.stat: - path: "{{ factory_koncreet_patch }}" - checksum_algorithm: sha256 - register: factory_koncreet_patch_stat - check_mode: false +- name: Install koncreet, or skip it with a warning + block: + - name: Hash the Ubuntu 26.04 koncreet patch + ansible.builtin.stat: + path: "{{ factory_koncreet_patch }}" + checksum_algorithm: sha256 + register: factory_koncreet_patch_stat + check_mode: false -# A new release or a changed patch lands in its own directory, so an unchanged -# pair is a no-op and /usr/local/bin/koncreet only moves once the new tree is -# complete. patch-outcome is written last and marks the tree complete. -- name: Name the koncreet release directory - ansible.builtin.set_fact: - factory_koncreet_dir: >- - {{ factory_koncreet_prefix }}/{{ factory_latest.koncreet.version - }}-{{ factory_koncreet_patch_stat.stat.checksum[:12] }} + # A new release or a changed patch lands in its own directory, so an unchanged + # pair is a no-op and /usr/local/bin/koncreet only moves once the new tree is + # complete. patch-outcome is written last and marks the tree complete. + - name: Name the koncreet release directory + ansible.builtin.set_fact: + factory_koncreet_dir: >- + {{ factory_koncreet_prefix }}/{{ factory_latest.koncreet.version + }}-{{ factory_koncreet_patch_stat.stat.checksum[:12] }} -- name: Stat the installed koncreet release - ansible.builtin.stat: - path: "{{ factory_koncreet_dir }}/patch-outcome" - register: factory_koncreet_stat - check_mode: false + - name: Stat the installed koncreet release + ansible.builtin.stat: + path: "{{ factory_koncreet_dir }}/patch-outcome" + register: factory_koncreet_stat + check_mode: false -- name: Install the latest koncreet release with the Ubuntu 26.04 patch - when: - - not factory_koncreet_stat.stat.exists - - not ansible_check_mode - become: true - block: - - name: Create the koncreet release directory - ansible.builtin.file: - path: "{{ factory_koncreet_dir }}" - state: directory - owner: root - group: root - mode: "0755" + - name: Install the latest koncreet release with the Ubuntu 26.04 patch + when: + - not factory_koncreet_stat.stat.exists + - not ansible_check_mode + become: true + block: + - name: Create the koncreet release directory + ansible.builtin.file: + path: "{{ factory_koncreet_dir }}" + state: directory + owner: root + group: root + mode: "0755" - - name: Fetch the koncreet tarball - ansible.builtin.get_url: - url: "{{ factory_koncreet.url }}" - dest: "{{ factory_koncreet_dir }}.tar.gz" - checksum: "sha256:{{ factory_koncreet.sha256 }}" - mode: "0600" + - name: Fetch the koncreet tarball + ansible.builtin.get_url: + url: "{{ factory_koncreet.url }}" + dest: "{{ factory_koncreet_dir }}.tar.gz" + checksum: "sha256:{{ factory_koncreet.sha256 }}" + mode: "0600" - - name: Extract koncreet - ansible.builtin.unarchive: - src: "{{ factory_koncreet_dir }}.tar.gz" - dest: "{{ factory_koncreet_dir }}" - remote_src: true - owner: root - group: root + - name: Extract koncreet + ansible.builtin.unarchive: + src: "{{ factory_koncreet_dir }}.tar.gz" + dest: "{{ factory_koncreet_dir }}" + remote_src: true + owner: root + group: root - - name: Remove the koncreet tarball - ansible.builtin.file: - path: "{{ factory_koncreet_dir }}.tar.gz" - state: absent + - name: Remove the koncreet tarball + ansible.builtin.file: + path: "{{ factory_koncreet_dir }}.tar.gz" + state: absent - # The patch never fails the apply: when upstream already supports 26.04 or - # the patch no longer applies, koncreet installs as released and the - # outcome says which case this release hit. - - name: Layer the Ubuntu 26.04 patch on the release - ansible.builtin.shell: | - cd koncreet || exit 1 - if grep -q '26\.04' lib/os.sh; then - echo "patch skipped: this release already supports Ubuntu 26.04" - elif git apply {{ factory_koncreet_patch | quote }} 2>/dev/null; then - echo "patch applied: Ubuntu 26.04 support layered on this release" - else - echo "patch skipped: it no longer applies, installed as released" - fi | tee ../patch-outcome.new && mv ../patch-outcome.new ../patch-outcome - args: - chdir: "{{ factory_koncreet_dir }}" - environment: - # Outside any repository, so git apply patches files like patch(1). - GIT_CEILING_DIRECTORIES: "{{ factory_koncreet_dir }}" - register: factory_koncreet_outcome - changed_when: true + # The patch never fails the apply: when upstream already supports 26.04 or + # the patch no longer applies, koncreet installs as released and the + # outcome says which case this release hit. + - name: Layer the Ubuntu 26.04 patch on the release + ansible.builtin.shell: | + cd koncreet || exit 1 + if grep -q '26\.04' lib/os.sh; then + echo "patch skipped: this release already supports Ubuntu 26.04" + elif git apply {{ factory_koncreet_patch | quote }} 2>/dev/null; then + echo "patch applied: Ubuntu 26.04 support layered on this release" + else + echo "patch skipped: it no longer applies, installed as released" + fi | tee ../patch-outcome.new && mv ../patch-outcome.new ../patch-outcome + args: + chdir: "{{ factory_koncreet_dir }}" + environment: + # Outside any repository, so git apply patches files like patch(1). + GIT_CEILING_DIRECTORIES: "{{ factory_koncreet_dir }}" + register: factory_koncreet_outcome + changed_when: true - - name: Report the koncreet release and patch outcome - ansible.builtin.debug: - msg: "koncreet {{ factory_latest.koncreet.version }}: {{ factory_koncreet_outcome.stdout }}" + - name: Report the koncreet release and patch outcome + ansible.builtin.debug: + msg: "koncreet {{ factory_latest.koncreet.version }}: {{ factory_koncreet_outcome.stdout }}" -- name: Link koncreet into /usr/local/bin - ansible.builtin.file: - src: "{{ factory_koncreet_dir }}/koncreet/koncreet" - dest: /usr/local/bin/koncreet - state: link - become: true - when: not ansible_check_mode + - name: Link koncreet into /usr/local/bin + ansible.builtin.file: + src: "{{ factory_koncreet_dir }}/koncreet/koncreet" + dest: /usr/local/bin/koncreet + state: link + become: true + when: not ansible_check_mode -- name: Render the fleet koncreet configuration once - ansible.builtin.template: - src: koncreet.conf.j2 - dest: "{{ factory_koncreet_config }}" - owner: root - group: root - mode: "0644" - force: false - become: true + - name: Render the fleet koncreet configuration once + ansible.builtin.template: + src: koncreet.conf.j2 + dest: "{{ factory_koncreet_config }}" + owner: root + group: root + mode: "0644" + force: false + become: true + + rescue: + - name: Skip koncreet + ansible.builtin.debug: + msg: >- + WARNING: koncreet skipped, apply continues. {{ ansible_failed_task.name }}: + {{ ansible_failed_result.msg | default('failed') }} diff --git a/ansible/tasks/preflight.yml b/ansible/tasks/preflight.yml index 6bdd4db..58470c0 100644 --- a/ansible/tasks/preflight.yml +++ b/ansible/tasks/preflight.yml @@ -103,6 +103,11 @@ ansible.builtin.set_fact: factory_latest: "{{ factory_latest_run.stdout | from_json }}" +- name: Warn about optional tools skipped by the lookup + ansible.builtin.debug: + msg: "{{ factory_latest_run.stderr_lines }}" + when: factory_latest_run.stderr | length > 0 + - name: Show the resolved provisioning plan ansible.builtin.debug: msg: diff --git a/ansible/templates/koncreet.conf.j2 b/ansible/templates/koncreet.conf.j2 index 7fbc8bf..35f447d 100644 --- a/ansible/templates/koncreet.conf.j2 +++ b/ansible/templates/koncreet.conf.j2 @@ -2,7 +2,9 @@ # (ansible/templates/koncreet.conf.j2). Edit it here: apply never overwrites it. # Nothing runs koncreet automatically. Run it by hand, in this order; the full # steps and the lockout recovery are in docs/security.md (Host hardening): +{% if factory_cfg.profiles.tailscale | bool %} # sudo ufw allow in on tailscale0 +{% endif %} # sudo koncreet doctor # sudo koncreet --dry-run apply -c {{ factory_koncreet_config }} # sudo koncreet apply -c {{ factory_koncreet_config }} @@ -12,21 +14,31 @@ modules=baseline,firewall,fail2ban,updates # The account that ran ./factory apply is the operator: it keeps the SSH keys -# it logs in with and is made a sudo user. Not factory.user, which runs the -# agents and must not gain sudo through this file. -{% if ansible_user_id != 'root' %} -user={{ ansible_user_id }} -pubkey_file={{ ansible_user_dir }}/.ssh/authorized_keys -{% else %} +# it logs in with and is made a sudo user. +{% if ansible_user_id == 'root' %} # Apply ran as root, so the operator account is unknown. Set both before use: # user= # pubkey_file=/home//.ssh/authorized_keys +{% elif ansible_user_id == factory_cfg.user %} +# Apply ran as {{ factory_cfg.user }}, the factory account that runs the agents and +# must not gain sudo through this file, so no sudo user is set. Set both to the +# operator's login before use: +# user= +# pubkey_file=/home//.ssh/authorized_keys +{% else %} +user={{ ansible_user_id }} +pubkey_file={{ ansible_user_dir }}/.ssh/authorized_keys {% endif %} -# SSH stays open: koncreet always allows the ports sshd listens on. 41641/udp -# is Tailscale's direct WireGuard port. Tailnet traffic arrives on tailscale0, -# which this file cannot name: run sudo ufw allow in on tailscale0 first. +# SSH stays open: koncreet always allows the ports sshd listens on. +{% if factory_cfg.profiles.tailscale | bool %} +# 41641/udp is Tailscale's direct WireGuard port. Tailnet traffic arrives on +# tailscale0, which this file cannot name: run sudo ufw allow in on tailscale0 +# first. firewall_ports=41641/udp +{% else %} +# The tailscale profile is off, so no Tailscale port or interface is opened. +{% endif %} firewall_public=false fail2ban_services=ssh diff --git a/containers/factory.container.yml b/containers/factory.container.yml index 2e94164..e973a5c 100644 --- a/containers/factory.container.yml +++ b/containers/factory.container.yml @@ -6,7 +6,8 @@ # container genuinely cannot host a native capability: # # start_services: false no systemd/D-Bus inside an ordinary container, so unit -# start/enable operations must be suppressed. +# start/enable operations must be suppressed. Koncreet, +# the host hardening toolkit, is not installed either. # enable_linger: false `loginctl enable-linger` requires a host user manager. # profiles.docker the worker never receives the host Docker socket and # does not run a nested daemon. diff --git a/docs/dependencies.md b/docs/dependencies.md index f977f38..65cf501 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -1,6 +1,6 @@ # Dependencies -Everything the recipe installs, grouped by where it comes from. Nothing is pinned: every `./factory apply` resolves each tool's newest release once and installs exactly that, so re-running apply upgrades an existing host. Every download is verified against the checksum its publisher posts for that exact release, and a release without one fails the apply instead of installing an unverified artifact. The one exception is the three omp marketplace plugins (ponytail, i-have-adhd, caveman): no publisher checksums them. The installer records the releases it resolved in `~/.local/share/code-factory/resolved.json`, which the container smoke compares against; `ansible/tasks/verify.yml` also asserts that the herdr service and omp run the resolved releases. `./factory plan` installs none of this. +Everything the recipe installs, grouped by where it comes from. Nothing is pinned: every `./factory apply` resolves each tool's newest release once and installs exactly that, so re-running apply upgrades an existing host. Every download is verified against the checksum its publisher posts for that exact release, and a release without one fails the apply instead of installing an unverified artifact (the optional Koncreet is skipped with a warning instead). The one exception is the three omp marketplace plugins (ponytail, i-have-adhd, caveman): no publisher checksums them. The installer records the releases it resolved in `~/.local/share/code-factory/resolved.json`, which the container smoke compares against; `ansible/tasks/verify.yml` also asserts that the herdr service and omp run the resolved releases. `./factory plan` installs none of this. ## Repository tooling @@ -45,9 +45,9 @@ The GitHub lookups use the GitHub API, which allows 60 unauthenticated requests ## Koncreet -Every host. +Every host that starts services (`start_services: true`); the container worker image skips it. -- [Koncreet](https://github.com/jimididit/koncreet), the latest release's `koncreet.tar.gz`, verified against the GitHub release-asset digest (`ansible/tasks/koncreet.yml`). It installs as root into `/usr/local/lib/code-factory/koncreet/-/` with `/usr/local/bin/koncreet` linked to it, and `patches/koncreet/ubuntu-26.04.patch` is layered on top. Apply never runs it; [Host hardening](security.md#host-hardening) has the manual run. +- [Koncreet](https://github.com/jimididit/koncreet), the latest release's `koncreet.tar.gz`, verified against the GitHub release-asset digest (`ansible/tasks/koncreet.yml`). It installs as root into `/usr/local/lib/code-factory/koncreet/-/` with `/usr/local/bin/koncreet` linked to it, and `patches/koncreet/ubuntu-26.04.patch` is layered on top. It is optional: when its lookup, checksum, or download fails, apply warns and skips it. Apply never runs it; [Host hardening](security.md#host-hardening) has the manual run. ## Chrome autopruner diff --git a/docs/security.md b/docs/security.md index 4166350..703f507 100644 --- a/docs/security.md +++ b/docs/security.md @@ -37,15 +37,15 @@ This export does not rewrite the current host's firewall, SSH policy, account me ## Host hardening -Every apply installs [Koncreet](https://github.com/jimididit/koncreet) as `/usr/local/bin/koncreet` and renders `/etc/koncreet.conf` once, but nothing runs it. It is a first-hour hardening toolkit: a sudo user with SSH keys, sysctl, swap, a journald cap, time sync, a ufw default-deny firewall, fail2ban on SSH, unattended security updates, and finally SSH with password and root login turned off. +Every apply on a host that starts services (not the container worker image) installs [Koncreet](https://github.com/jimididit/koncreet) as `/usr/local/bin/koncreet` and renders `/etc/koncreet.conf` once, but nothing runs it. Koncreet is optional: when its release lookup, checksum, or download fails, apply prints a warning, skips it, and finishes the rest; a release installed earlier stays in place. It is a first-hour hardening toolkit: a sudo user with SSH keys, sysctl, swap, a journald cap, time sync, a ufw default-deny firewall, fail2ban on SSH, unattended security updates, and finally SSH with password and root login turned off. Upstream supports Debian 12/13 and Ubuntu 22.04/24.04 only. `patches/koncreet/ubuntu-26.04.patch` adds Ubuntu 26.04: it opens the OS gate and doctor, and restores the last fallback Koncreet uses to find your SSH client address for the fail2ban whitelist: 26.04 keeps no utmp, so `who -m` prints nothing, and the patch asks logind instead. The same change is the `ubuntu-26.04` branch of the [undeemed/koncreet](https://github.com/undeemed/koncreet/tree/ubuntu-26.04) fork; regenerate the patch from there with `git diff main...ubuntu-26.04`. Apply layers the patch on each new release and prints which case it hit: applied; skipped because the release already supports 26.04; or skipped because it no longer applies, in which case Koncreet installs as released and refuses to run on 26.04 until the patch is refreshed. The patch never fails the apply. -`/etc/koncreet.conf` makes the account that ran `./factory apply` the sudo user, installs the SSH keys it logs in with, keeps SSH open (Koncreet always allows the ports sshd listens on) and opens 41641/udp for Tailscale's direct connections. Apply never overwrites it; edit it there. +`/etc/koncreet.conf` makes the account that ran `./factory apply` the sudo user, installs the SSH keys it logs in with, and keeps SSH open (Koncreet always allows the ports sshd listens on). With the `tailscale` profile it also opens 41641/udp for Tailscale's direct connections. When apply ran as root, or as the factory account (which runs the agents and must not gain sudo), no sudo user is set: `user=` and `pubkey_file=` stay commented out until you fill in the operator's login. Apply never overwrites it; edit it there. Run it once, by hand, from an SSH session you keep open until the last step works: -1. `sudo ufw allow in on tailscale0`, so the tailnet stays reachable once ufw denies incoming traffic. Koncreet keeps existing ufw rules. +1. With the `tailscale` profile: `sudo ufw allow in on tailscale0`, so the tailnet stays reachable once ufw denies incoming traffic. Koncreet keeps existing ufw rules. 2. `sudo koncreet doctor` 3. `sudo koncreet --dry-run apply -c /etc/koncreet.conf`, and read the plan. 4. `sudo koncreet apply -c /etc/koncreet.conf` @@ -67,6 +67,6 @@ Logs: `/var/log/koncreet.log`. Backups: `*.koncreet.bak`. The provider's console ## Updates -Tools track their latest release, and every download is verified against the checksum its publisher posts for that release (what each source checks is in [Dependencies](dependencies.md)); a release without one is refused. The one exception is the three omp marketplace plugins (ponytail, i-have-adhd, caveman): no publisher checksums them, they track each author's default branch, and they load as agent instructions and hooks. The operator accepted that to keep them at the latest commit. Checksums prove a download is the published artifact; they do not establish that a publisher is trustworthy. Review added tools and installer behavior before adding them. Ubuntu security updates remain an operating-system responsibility rather than freezing an entire vulnerable package index forever. +Tools track their latest release, and every download is verified against the checksum its publisher posts for that release (what each source checks is in [Dependencies](dependencies.md)); a release without one is refused (Koncreet, being optional, is skipped with a warning instead). The one exception is the three omp marketplace plugins (ponytail, i-have-adhd, caveman): no publisher checksums them, they track each author's default branch, and they load as agent instructions and hooks. The operator accepted that to keep them at the latest commit. Checksums prove a download is the published artifact; they do not establish that a publisher is trustworthy. Review added tools and installer behavior before adding them. Ubuntu security updates remain an operating-system responsibility rather than freezing an entire vulnerable package index forever. Back up project repositories and application data separately, using encrypted storage and an application-aware restore procedure. A successful environment bootstrap is not evidence that a database backup is recoverable. diff --git a/scripts/install_tools.py b/scripts/install_tools.py index 9a37257..8ccafbd 100755 --- a/scripts/install_tools.py +++ b/scripts/install_tools.py @@ -73,6 +73,8 @@ # Resolved for host hardening (ansible/tasks/koncreet.yml), installed there as root. "koncreet": ("jimididit/koncreet", "v", "koncreet.tar.gz", {"koncreet": "koncreet/koncreet"}), } +# Resolved when they can be, skipped with a warning when they cannot: they never stop a run. +OPTIONAL = {"koncreet"} # npm tools on the registry's latest version, each installed into its own prefix. NPM_LATEST = { "omp": "@oh-my-pi/pi-coding-agent", @@ -141,24 +143,33 @@ def verified(tool, version, key, name, url, checksum, binaries): def resolve_latest(key, names): """Specs for the newest releases of exactly these tools, and nothing else.""" latest = {} + skipped = set() for tool, (repo, prefix, pattern, binaries) in GITHUB_LATEST.items(): if tool not in names: continue - if tool in PRERELEASE_CHANNEL: - releases = json.loads( - fetch(GITHUB_API.format(repo) + "?per_page=10", f"{tool} release") - ) - release = next((r for r in releases if not r["draft"]), None) - if release is None: - raise ValueError(f"{tool} has no published release") - else: - release = json.loads(fetch(GITHUB_API.format(repo) + "/latest", f"{tool} release")) - version = release["tag_name"].removeprefix(prefix) - name = pattern.format(v=version, key=key, **ARCH[key]) - asset = next((a for a in release["assets"] if a["name"] == name), {}) - checksum = (asset.get("digest") or "").removeprefix("sha256:") - url = asset.get("browser_download_url", "") - latest[tool] = verified(tool, version, key, name, url, checksum, binaries) + try: + if tool in PRERELEASE_CHANNEL: + releases = json.loads( + fetch(GITHUB_API.format(repo) + "?per_page=10", f"{tool} release") + ) + release = next((r for r in releases if not r["draft"]), None) + if release is None: + raise ValueError(f"{tool} has no published release") + else: + release = json.loads( + fetch(GITHUB_API.format(repo) + "/latest", f"{tool} release") + ) + version = release["tag_name"].removeprefix(prefix) + name = pattern.format(v=version, key=key, **ARCH[key]) + asset = next((a for a in release["assets"] if a["name"] == name), {}) + checksum = (asset.get("digest") or "").removeprefix("sha256:") + url = asset.get("browser_download_url", "") + latest[tool] = verified(tool, version, key, name, url, checksum, binaries) + except (OSError, ValueError, KeyError) as error: + if tool not in OPTIONAL: + raise + skipped.add(tool) + print(f"install_tools: skipping optional {tool}: {error}", file=sys.stderr) if "node" in names: releases = json.loads(fetch("https://nodejs.org/dist/index.json", "node release")) tag = max((r["version"] for r in releases), key=lambda v: tuple(map(int, v[1:].split(".")))) @@ -202,7 +213,7 @@ def resolve_latest(key, names): if not hashes or not all(re.fullmatch(r"[0-9a-f]{64}", h) for h in hashes): raise ValueError("psutil publishes no SHA-256 digests; refusing an unverified binary") latest["psutil"] = {"version": pypi["info"]["version"], "sha256": hashes} - if unknown := sorted(set(names) - latest.keys()): + if unknown := sorted(set(names) - latest.keys() - skipped): raise ValueError(f"no latest release source for: {', '.join(unknown)}") return latest diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 30ee5fd..c8f1c3f 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -4,6 +4,7 @@ import shutil import subprocess import sys +import tarfile from pathlib import Path import pytest @@ -594,3 +595,164 @@ def test_a_new_chrome_devtools_mcp_release_leaves_the_managed_environment_unchan assert before["CHROME_DEVTOOLS_AXI_MCP_PATH"].endswith( "/chrome-devtools-mcp/current/node_modules/chrome-devtools-mcp/build/src/bin/chrome-devtools-mcp.js" ) + + +def _ansible(tmp_path, *argv): + return subprocess.run( + [Path(sys.executable).parent / argv[0], *argv[1:]], + cwd=tmp_path, + capture_output=True, + text=True, + ) + + +@pytest.mark.parametrize("start_services", [True, False]) +def test_koncreet_is_resolved_only_on_hosts_that_start_services(tmp_path, start_services): + variables = { + "factory_cfg": { + "start_services": start_services, + "profiles": {"agents": False, "fleet_guards": False}, + "browser_prune": {"enabled": False}, + } + } + result = _ansible( + tmp_path, + "ansible", + "localhost", + "-i", + "localhost,", + "-c", + "local", + "-m", + "ansible.builtin.debug", + "-a", + "var=factory_installer_also", + "-e", + f"@{ROOT / 'ansible/group_vars/all.yml'}", + "-e", + json.dumps(variables), + ) + assert result.returncode == 0, result.stdout + also = json.loads(result.stdout.split("=>", 1)[1])["factory_installer_also"] + assert ("koncreet" in also) is start_services + + +def _koncreet_settings(tmp_path, tailscale, apply_user): + destination = tmp_path / "koncreet.conf" + variables = { + "ansible_user_id": apply_user, + "ansible_user_dir": "/root" if apply_user == "root" else f"/home/{apply_user}", + "factory_cfg": {"user": "coder", "profiles": {"tailscale": tailscale}}, + "factory_koncreet_config": "/etc/koncreet.conf", + } + result = _ansible( + tmp_path, + "ansible", + "localhost", + "-i", + "localhost,", + "-c", + "local", + "-m", + "ansible.builtin.template", + "-a", + f"src={ROOT / 'ansible/templates/koncreet.conf.j2'} dest={destination}", + "-e", + json.dumps(variables), + ) + assert result.returncode == 0, result.stdout + return dict( + line.split("=", 1) + for line in destination.read_text().splitlines() + if line and not line.startswith("#") + ) + + +@pytest.mark.parametrize( + ("tailscale", "apply_user", "ports", "sudo_user"), + [ + (True, "operator", "41641/udp", "operator"), + (False, "operator", None, "operator"), + (True, "coder", "41641/udp", None), + (False, "root", None, None), + ], +) +def test_koncreet_config_follows_the_tailscale_profile_and_never_makes_the_factory_user_sudo( + tmp_path, tailscale, apply_user, ports, sudo_user +): + settings = _koncreet_settings(tmp_path, tailscale, apply_user) + assert settings["modules"] == "baseline,firewall,fail2ban,updates" + assert settings.get("firewall_ports") == ports + assert settings.get("user") == sudo_user + assert settings.get("pubkey_file") == ( + f"/home/{sudo_user}/.ssh/authorized_keys" if sudo_user else None + ) + + +def _run_koncreet_tasks(tmp_path, digest, *flags): + payload = tmp_path / "payload" + payload.write_text("#!/bin/sh\n") + source = tmp_path / "koncreet.tar.gz" + with tarfile.open(source, "w:gz") as archive: + archive.add(payload, arcname="koncreet/koncreet") + (tmp_path / "templates").symlink_to(ROOT / "ansible/templates") + playbook = tmp_path / "playbook.yml" + playbook.write_text( + yaml.safe_dump( + [ + { + "hosts": "localhost", + "connection": "local", + "gather_facts": False, + "tasks": [ + {"ansible.builtin.import_tasks": str(ROOT / "ansible/tasks/koncreet.yml")}, + { + "name": "A later task", + "ansible.builtin.file": { + "path": str(tmp_path / "later"), + "state": "touch", + }, + }, + ], + } + ] + ) + ) + variables = { + "ansible_become": False, + "ansible_user_id": "operator", + "ansible_user_dir": "/home/operator", + "factory_cfg": {"user": "coder", "profiles": {"tailscale": False}}, + "factory_latest": {"koncreet": {"version": "9.9.9"}}, + "factory_koncreet": {"url": source.as_uri(), "sha256": digest}, + "factory_koncreet_patch": str(ROOT / "patches/koncreet/ubuntu-26.04.patch"), + "factory_koncreet_prefix": str(tmp_path / "prefix"), + "factory_koncreet_config": str(tmp_path / "koncreet.conf"), + } + return _ansible( + tmp_path, + "ansible-playbook", + "-i", + "localhost,", + str(playbook), + "--extra-vars", + json.dumps(variables), + *flags, + ) + + +def test_a_koncreet_tarball_that_fails_its_digest_is_skipped_and_apply_continues(tmp_path): + result = _run_koncreet_tasks(tmp_path, "0" * 64) + assert result.returncode == 0, result.stdout + assert "WARNING: koncreet skipped" in result.stdout + assert (tmp_path / "later").exists() + assert not list((tmp_path / "prefix").rglob("patch-outcome")) + assert not list((tmp_path / "prefix").rglob("*.tar.gz")) + assert not (tmp_path / "koncreet.conf").exists() + + +def test_planning_koncreet_installs_nothing_and_warns_of_nothing(tmp_path): + result = _run_koncreet_tasks(tmp_path, "0" * 64, "--check") + assert result.returncode == 0, result.stdout + assert "WARNING" not in result.stdout + assert not (tmp_path / "prefix").exists() diff --git a/tests/test_install_tools.py b/tests/test_install_tools.py index e2858ce..1000d5d 100644 --- a/tests/test_install_tools.py +++ b/tests/test_install_tools.py @@ -226,21 +226,80 @@ def urlopen(request, **kwargs): installer.resolve_latest("linux-x86_64", {"no-mistakes"}) -@pytest.mark.parametrize("source", [*RELEASES, "node", "rustup-init", "psutil"]) +@pytest.mark.parametrize( + "source", + [*(repo for repo in RELEASES if repo != "jimididit/koncreet"), "node", "rustup-init", "psutil"], +) def test_release_without_a_published_checksum_is_refused(monkeypatch, source): upstream(monkeypatch, unverified=source) with pytest.raises(ValueError, match="refusing an unverified binary"): installer.resolve_latest("linux-x86_64", EVERYTHING) -@pytest.mark.parametrize("tag", ["v1.0$(id)", "v1;id", 'v1"x', "v1`id`", "v1|id", "v1 2", "v/1"]) +UNSAFE_TAGS = ["v1.0$(id)", "v1;id", 'v1"x', "v1`id`", "v1|id", "v1 2", "v/1"] + + +@pytest.mark.parametrize("tag", UNSAFE_TAGS) def test_release_tag_that_is_not_a_plain_version_is_refused(monkeypatch, tag): - monkeypatch.setitem(RELEASES, "jimididit/koncreet", (tag, "koncreet.tar.gz")) + monkeypatch.setitem(RELEASES, "herdrdev/herdr", (tag, "herdr-linux-x86_64")) upstream(monkeypatch) with pytest.raises(ValueError, match="not a safe version"): installer.resolve_latest("linux-x86_64", EVERYTHING) +def resolve_without_koncreet(capsys): + latest = installer.resolve_latest("linux-x86_64", EVERYTHING) + assert "koncreet" not in latest + assert {"herdr", "uv", "obscura", "node", "psutil"} <= latest.keys() + return capsys.readouterr().err + + +@pytest.mark.parametrize("tag", UNSAFE_TAGS) +def test_koncreet_release_with_an_unsafe_tag_is_skipped_with_a_warning(monkeypatch, capsys, tag): + monkeypatch.setitem(RELEASES, "jimididit/koncreet", (tag, "koncreet.tar.gz")) + upstream(monkeypatch) + assert "not a safe version" in resolve_without_koncreet(capsys) + + +def test_koncreet_release_without_a_published_checksum_is_skipped_with_a_warning( + monkeypatch, capsys +): + upstream(monkeypatch, unverified="jimididit/koncreet") + assert "refusing an unverified binary" in resolve_without_koncreet(capsys) + + +def test_koncreet_release_without_its_asset_is_skipped_with_a_warning(monkeypatch, capsys): + monkeypatch.setitem(RELEASES, "jimididit/koncreet", ("v9.9.9", "renamed.tar.gz")) + upstream(monkeypatch) + assert "refusing an unverified binary" in resolve_without_koncreet(capsys) + + +@pytest.mark.parametrize( + "failure", + [ + installer.urllib.error.HTTPError("https://api.github.com/", 403, "rate limited", {}, None), + installer.urllib.error.URLError("offline"), + ], +) +def test_koncreet_lookup_failure_is_skipped_with_a_warning(monkeypatch, capsys, failure): + upstream(monkeypatch) + fake = installer.urllib.request.urlopen + + def urlopen(request, **kwargs): + if "jimididit/koncreet" in request.full_url: + raise failure + return fake(request, **kwargs) + + monkeypatch.setattr(installer.urllib.request, "urlopen", urlopen) + assert "skipping optional koncreet" in resolve_without_koncreet(capsys) + + +def test_resolve_cli_still_succeeds_when_koncreet_cannot_be_resolved(monkeypatch, capsys, tmp_path): + upstream(monkeypatch, unverified="jimididit/koncreet") + latest = run_cli(monkeypatch, capsys, tmp_path, "--tools", "uv", "--also", "koncreet") + assert set(latest) == {"uv"} + + @pytest.mark.parametrize(("env", "expected"), [("env-token", "Bearer env-token"), ("", None)]) def test_github_token_goes_only_to_the_github_api(monkeypatch, env, expected): seen = [] From 29b71e282d276faae78bfb4af4353c2264650426 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 1 Oct 2026 09:33:44 +0000 Subject: [PATCH 5/7] no-mistakes(test): Install fakeroot in CI for koncreet digest test --- .github/workflows/ci.yml | 6 ++++++ tests/test_configuration.py | 13 +++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5b7f25..b7567bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,12 @@ jobs: - name: Lint run: uv run ruff check . + # tests/test_configuration.py runs koncreet.yml under fakeroot when not root. + - name: Install fakeroot + run: | + sudo apt-get update + sudo apt-get install --yes --no-install-recommends fakeroot + - name: Python tests run: uv run pytest diff --git a/tests/test_configuration.py b/tests/test_configuration.py index c8f1c3f..d4970d5 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1,6 +1,7 @@ import argparse import importlib.util import json +import os import shutil import subprocess import sys @@ -597,9 +598,9 @@ def test_a_new_chrome_devtools_mcp_release_leaves_the_managed_environment_unchan ) -def _ansible(tmp_path, *argv): +def _ansible(tmp_path, *argv, wrapper=()): return subprocess.run( - [Path(sys.executable).parent / argv[0], *argv[1:]], + [*wrapper, Path(sys.executable).parent / argv[0], *argv[1:]], cwd=tmp_path, capture_output=True, text=True, @@ -729,6 +730,12 @@ def _run_koncreet_tasks(tmp_path, digest, *flags): "factory_koncreet_prefix": str(tmp_path / "prefix"), "factory_koncreet_config": str(tmp_path / "koncreet.conf"), } + # koncreet.yml hands its files to root. Unprivileged, the chown fails before the + # download is ever checked, so a digest test would pass for the wrong reason. + # Plan mode (--check) creates nothing and needs no root. + wrapper = () if os.geteuid() == 0 or "--check" in flags else ("fakeroot",) + if wrapper and not shutil.which("fakeroot"): + pytest.skip("koncreet.yml chowns to root: run as root or install fakeroot") return _ansible( tmp_path, "ansible-playbook", @@ -738,6 +745,7 @@ def _run_koncreet_tasks(tmp_path, digest, *flags): "--extra-vars", json.dumps(variables), *flags, + wrapper=wrapper, ) @@ -745,6 +753,7 @@ def test_a_koncreet_tarball_that_fails_its_digest_is_skipped_and_apply_continues result = _run_koncreet_tasks(tmp_path, "0" * 64) assert result.returncode == 0, result.stdout assert "WARNING: koncreet skipped" in result.stdout + assert "Fetch the koncreet tarball: The checksum for" in result.stdout assert (tmp_path / "later").exists() assert not list((tmp_path / "prefix").rglob("patch-outcome")) assert not list((tmp_path / "prefix").rglob("*.tar.gz")) From 269428a708b95326a4ffc686f01a146facaebc76 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 1 Oct 2026 10:22:29 +0000 Subject: [PATCH 6/7] no-mistakes(document): Point architecture sources at Dependencies; note fakeroot for tests --- CONTRIBUTING.md | 2 +- docs/architecture.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06abeb7..8ae9a3f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,7 +35,7 @@ uv sync --group dev # Lint uv run ruff check scripts tests -# Test +# Test (the Koncreet apply tests need root or fakeroot; unprivileged without it they skip) uv run pytest # Ansible syntax check diff --git a/docs/architecture.md b/docs/architecture.md index 085815c..fad9e05 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -75,7 +75,7 @@ Every action is pinned to the commit SHA of its latest release (Dependabot moves ## Primary sources - [Herdr installation](https://herdr.dev/docs/install/), [headless/SSH persistence](https://herdr.dev/docs/persistence-remote/), [session-state limits](https://herdr.dev/docs/session-state/), [config reference](https://herdr.dev/docs/config-reference/). -- [Herdr latest release](https://github.com/herdrdev/herdr/releases/latest). GitHub-hosted assets (herdr, bun, uv, gh, no-mistakes, treehouse, Obscura) are verified against the SHA-256 digest GitHub publishes for each release asset; no claim is made that a release supplies an independent SBOM or signature bundle. +- [Herdr latest release](https://github.com/herdrdev/herdr/releases/latest). GitHub-hosted assets (the tools in [Dependencies](dependencies.md)) are verified against the SHA-256 digest GitHub publishes for each release asset; no claim is made that a release supplies an independent SBOM or signature bundle. - [Node.js release index](https://nodejs.org/dist/index.json). Node assets are verified against the `SHASUMS256.txt` published beside each release. - [rustup stable release](https://static.rust-lang.org/rustup/release-stable.toml). rustup-init is verified against the `.sha256` published beside it. - [Ansible introduction](https://docs.ansible.com/projects/ansible/latest/getting_started/index.html), [checksummed downloads](https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/get_url_module.html), [user systemd/D-Bus requirements](https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/systemd_service_module.html). From b3d31fe445790a7c9a1f770f0ccd984a9d2eeff6 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 1 Oct 2026 10:53:43 +0000 Subject: [PATCH 7/7] no-mistakes(document): Format install_tools.py; docs already match code --- scripts/install_tools.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/install_tools.py b/scripts/install_tools.py index 8ccafbd..14e8fae 100755 --- a/scripts/install_tools.py +++ b/scripts/install_tools.py @@ -156,9 +156,7 @@ def resolve_latest(key, names): if release is None: raise ValueError(f"{tool} has no published release") else: - release = json.loads( - fetch(GITHUB_API.format(repo) + "/latest", f"{tool} release") - ) + release = json.loads(fetch(GITHUB_API.format(repo) + "/latest", f"{tool} release")) version = release["tag_name"].removeprefix(prefix) name = pattern.format(v=version, key=key, **ARCH[key]) asset = next((a for a in release["assets"] if a["name"] == name), {})