Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 15 additions & 2 deletions ansible/group_vars/all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'] 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 []) }}

# --- Herdr static config ----------------------------------------------------
Expand Down Expand Up @@ -361,6 +362,18 @@ 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. 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
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)
Expand Down
5 changes: 5 additions & 0 deletions ansible/site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@
when: factory_cfg.profiles.tailscale | bool
tags: [tailscale]

- 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
ansible.builtin.import_tasks: tasks/browser.yml
when: >-
Expand Down
118 changes: 118 additions & 0 deletions ansible/tasks/koncreet.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
# 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: 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] }}

- 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: 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: 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

rescue:
- name: Skip koncreet
ansible.builtin.debug:
msg: >-
WARNING: koncreet skipped, apply continues. {{ ansible_failed_task.name }}:
{{ ansible_failed_result.msg | default('failed') }}
7 changes: 6 additions & 1 deletion ansible/tasks/preflight.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ~ '/')
Expand Down Expand Up @@ -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:
Expand Down
45 changes: 45 additions & 0 deletions ansible/templates/koncreet.conf.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 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):
{% 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 }}
# 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 is the operator: it keeps the SSH keys
# 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=<operator login>
# pubkey_file=/home/<operator login>/.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=<operator login>
# pubkey_file=/home/<operator login>/.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.
{% 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
auto_reboot=false
3 changes: 2 additions & 1 deletion containers/factory.container.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
10 changes: 8 additions & 2 deletions docs/dependencies.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

Expand All @@ -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-<version>/`.
- 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 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/<version>-<patch hash>/` 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

`agents` profile with `browser_prune.enabled`.
Expand Down
Loading