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
17 changes: 17 additions & 0 deletions Containerfile.tether
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Project-owned standard-tools transport; immutable deployment uses its built digest.
FROM docker.io/library/debian@sha256:c67e92c8099ba22853ab4cfda7cfb494d253d6012ca82dee98ba8976772af018
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 wireguard-tools iproute2 nftables openssh-server passwd \
&& rm -rf /var/lib/apt/lists/* /etc/ssh/ssh_host_* \
&& groupadd --system tether \
&& useradd --system --gid tether --home-dir /nonexistent --shell /usr/sbin/nologin --password '*' tether \
&& mkdir -p /usr/libexec /etc/lychd-tether /var/empty/sshd \
&& chmod 0755 /var/empty/sshd /etc/lychd-tether
COPY src/lychd/system/services/tether/runtime.py /usr/libexec/lychd_tether_runtime.py
COPY LICENSE THIRD_PARTY_NOTICES.md /usr/share/doc/lychd-tether/
RUN chmod 0555 /usr/libexec/lychd_tether_runtime.py \
&& test -x /usr/sbin/ip && test -x /usr/sbin/nft \
&& test -x /usr/bin/wg && test -x /usr/sbin/sshd
USER 0
ENV PYTHONPATH=/usr/libexec
CMD ["/usr/bin/python3", "-m", "lychd_tether_runtime", "/etc/lychd-tether/config.json"]
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ CI_PYTEST_TARGETS := tests/unit/config tests/unit/db \
tests/unit/cli/test_reach_observation.py tests/unit/system/services/test_oculus_snapshot.py \
tests/unit/cli/test_reach_operator_onboarding.py \
tests/unit/system/services/test_intercom_http.py \
tests/unit/domain/tether tests/unit/cli/test_tether.py \
tests/unit/system/services/test_tether_runtime.py tests/unit/system/services/test_tether_consumer.py \
tests/unit/system/services/test_tether_broker_tls.py \
tests/unit/system/services/test_tether_custody.py tests/unit/system/services/test_tether_keys.py \
tests/unit/system/services/test_tether_host.py tests/unit/system/services/test_tether_service.py \
tests/unit/system/services/test_secrets.py tests/unit/system/test_container_security.py \
tests/unit/system/services/test_bind.py tests/unit/system/services/test_codex.py \
tests/unit/domain/test_artifact_custody.py tests/unit/domain/test_evolution.py \
tests/unit/domain/animation tests/unit/domain/cortex tests/unit/domain/delegation \
tests/unit/domain/test_naming.py \
Expand Down
8 changes: 6 additions & 2 deletions deployments/ansible/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ setup, private inventory and variables, check/apply commands, limitations and ho
| --- | --- | --- |
| `host.yml` / `verify.yml` | administrator + sudo | Converge / observe the installed host floor. |
| `source.yml` | direct `lychd` SSH | Stage one exact source CLI while the body is inactive. |
| `app-verify.yml` | direct `lychd` SSH | Read-only application acceptance of a running body. |
| `app-verify.yml` | direct `lychd` SSH; administrator for protocol-2 host prerequisites | Read-only application and selected host-prerequisite acceptance. |
| `deploy.yml` | both, one `--limit`ed host | Classify, then provision a fresh host or only verify a healthy one. |
| `server.yml` | same as `deploy.yml` | Explicit headless Server entry point. |
| `admin.yml` | administrator + sudo, explicit target | Replace only stock vendor sudo policy with a named administrator policy. |
Expand All @@ -27,6 +27,10 @@ The [deployment index](../../docs/deployments/index.md) routes installation, Ser
private access and acceptance. Private inventories, password hashes, endpoint keys, generated
AutoYaST profiles and image archives belong outside the checkout, including ignored directories.

Controller and installed CLI must both implement deployment protocol 1. An older pilot whose
For the default profile, controller and installed CLI must both implement deployment protocol 1. An older pilot whose
settings were edited after init is deliberately refused; retain its previous private controller
for read-only observation. No automated settings migration or receipt rewrite is provided.

For the explicit native Tether initialization and deployment protocol 2, follow
[the Tether profile](../../docs/deployments/tumbleweed/index.md#native-tether-profile-protocol-2).
The default remains protocol 1; image admission accepts `--tether` for a separate runtime image receipt.
20 changes: 19 additions & 1 deletion deployments/ansible/playbooks/app-verify.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
# Read-only application acceptance over direct runtime SSH; no sudo, no effects.
# Read-only application acceptance; protocol 2 also observes the administrator-owned kernel and UDP prerequisites.
- name: Observe one LychD body without changing it
hosts: lychd_hosts
gather_facts: false
Expand All @@ -25,3 +25,21 @@
ansible.builtin.include_role:
name: lychd_app
tasks_from: verify

- name: Observe the protocol-2 host prerequisites without changing them
hosts: lychd_hosts
gather_facts: false
become: true
serial: 1
tasks:
- name: Verify the host WireGuard module and boot entry without mutation
ansible.builtin.include_role:
name: lychd_app
tasks_from: tether_kernel_verify
when: lychd_app_profile | default('empty-extensions-container') == 'tether-container-v1'

- name: Verify the normalized listener's effective OS firewall zone and port
ansible.builtin.include_role:
name: lychd_app
tasks_from: firewall_verify
when: lychd_app_profile | default('empty-extensions-container') == 'tether-container-v1'
59 changes: 59 additions & 0 deletions deployments/ansible/playbooks/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,49 @@
name: lychd_image
when: lychd_deploy_mode == 'mutate' and lychd_deploy_runtime_ready | bool

- name: Stage the verified normalized Tether campaign before host UDP preparation
ansible.builtin.include_role:
name: lychd_app
tasks_from: tether_stage
when: >-
lychd_app_profile | default('empty-extensions-container') == 'tether-container-v1'
and lychd_deploy_mode == 'mutate' and lychd_deploy_runtime_ready | bool
and not ansible_check_mode

- name: Admit only declared host prerequisites before native initialization
hosts: lychd_hosts
gather_facts: false
become: true
tasks:
- name: Prepare the host WireGuard module without managing VPN state
ansible.builtin.include_role:
name: lychd_app
tasks_from: tether_kernel_prepare
when: >-
lychd_app_profile | default('empty-extensions-container') == 'tether-container-v1'
and lychd_deploy_mode == 'mutate'
and not (ansible_check_mode and lychd_app_observed.state == 'no_runtime_account')

- name: Prepare the existing host zone without managing VPN state
ansible.builtin.include_role:
name: lychd_app
tasks_from: firewall
when: >-
lychd_app_profile | default('empty-extensions-container') == 'tether-container-v1'
and lychd_deploy_mode == 'mutate'
and not (ansible_check_mode and lychd_app_observed.state == 'no_runtime_account')

- name: Apply native application lifecycle after host prerequisites
hosts: lychd_hosts
gather_facts: false
connection: ssh
remote_user: lychd
become: false
vars:
ansible_user: lychd
ansible_become: false
lychd_deploy_runtime_ready: "{{ not (ansible_check_mode and lychd_app_observed.state == 'no_runtime_account') }}"
tasks:
- name: Initialize, configure, bind and (when authorized) start
ansible.builtin.include_role:
name: lychd_app
Expand All @@ -219,6 +262,22 @@
tags: [floor]
when: lychd_deploy_mode == 'verify'

- name: Verify the host WireGuard module and boot entry without mutation
ansible.builtin.include_role:
name: lychd_app
tasks_from: tether_kernel_verify
when: >-
lychd_app_profile | default('empty-extensions-container') == 'tether-container-v1'
and lychd_deploy_mode == 'verify'

- name: Verify the existing normalized host UDP prerequisite without mutation
ansible.builtin.include_role:
name: lychd_app
tasks_from: firewall_verify
when: >-
lychd_app_profile | default('empty-extensions-container') == 'tether-container-v1'
and lychd_deploy_mode == 'verify'

- name: Observe application acceptance
hosts: lychd_hosts
gather_facts: false
Expand Down
10 changes: 10 additions & 0 deletions deployments/ansible/roles/lychd_app/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,13 @@ lychd_app_start_authorized: false
# Bounded read-only convergence wait after start; no restart, kill or second lifecycle is attempted.
lychd_app_start_poll_retries: 36
lychd_app_start_poll_delay: 10

# Protocol 1 remains the default; protocol 2 must be selected explicitly.
lychd_app_profile: empty-extensions-container
lychd_tether_intent: ""
lychd_tether_intent_sha256: ""
lychd_tether_image_id: ""
lychd_tether_intent_remote: /home/lychd/.local/state/lychd-deploy/tether-intent.json

# Explicit authorization for one existing OS firewall zone; no separate port input.
lychd_tether_firewall_zone: ""
34 changes: 27 additions & 7 deletions deployments/ansible/roles/lychd_app/files/app_probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,14 @@

SCHEMA = "lychd.deployment"
VERSION = 1
STATES = {"absent", "initialized", "bound_inactive", "running", "foreign_revision", "unknown"}
ACTIONS = {"absent": "init", "initialized": "bind", "bound_inactive": "start", "running": "observe"}
STATES = {"absent", "initialized", "bound_inactive", "running", "foreign_revision", "unknown", "prepared_waiting_keys"}
ACTIONS = {
"absent": "init",
"initialized": "bind",
"bound_inactive": "start",
"running": "observe",
"prepared_waiting_keys": "import-key",
}


class Refusal(Exception):
Expand All @@ -39,18 +45,32 @@ def native_command(command, request, home):
if identifier:
require(re.fullmatch(r"(?:sha256:)?[0-9a-f]{64}", identifier), "exact image ID required")
argv.extend(["--deployment-image-id", identifier])
profile = request.get("profile", "empty-extensions-container")
require(profile in {"empty-extensions-container", "tether-container-v1"}, "unsupported deployment profile")
if profile == "tether-container-v1":
intent = request.get("intent", "")
image = request.get("tether_image_id", "")
require(isinstance(intent, str) and Path(intent).is_absolute(), "absolute public intent required")
require(re.fullmatch(r"[0-9a-f]{64}", image), "admitted Tether image required")
argv.extend(
["--deployment-profile", profile, "--deployment-intent", intent, "--deployment-tether-image-id", image]
)
if command == "verify":
argv.append("--deployment-verify")
return source, argv


def validate_response(document):
def validate_response(document, profile="empty-extensions-container"):
"""Require the exact native protocol without synthesizing lifecycle truth."""
require(isinstance(document, dict), "native response is not an object")
require(
document.get("schema") == SCHEMA and type(document.get("version")) is int and document["version"] == VERSION,
"native deployment protocol 1 required",
document.get("schema") == SCHEMA
and type(document.get("version")) is int
and document["version"] == (2 if profile == "tether-container-v1" else VERSION),
"matching native deployment protocol required",
)
if profile == "tether-container-v1":
require(document.get("profile") == profile, "native profile mismatch")
state = document.get("state")
require(state in STATES, "unsupported native deployment state")
require(document.get("next_action") == ACTIONS.get(state, "refuse"), "inconsistent native action")
Expand Down Expand Up @@ -80,7 +100,7 @@ def observe(command, request, home):
raise Refusal("native deployment observation unavailable") from error
require(result.returncode in {0, 1}, "native deployment observation failed")
try:
document = validate_response(json.loads(result.stdout))
document = validate_response(json.loads(result.stdout), request.get("profile", "empty-extensions-container"))
except (ValueError, TypeError, KeyError) as error:
raise Refusal("native deployment response unavailable or incompatible") from error
require(document.get("revision", request["revision"]) == request["revision"], "native revision mismatch")
Expand All @@ -101,4 +121,4 @@ def main():
try:
sys.exit(main())
except (Refusal, ValueError, TypeError, KeyError, IndexError, OSError):
sys.exit("lychd deployment protocol observation refused; matching controller and native protocol 1 required")
sys.exit("lychd deployment protocol observation refused; matching controller and native protocol required")
Loading
Loading