@crvouga/dev-fleet 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -7
- package/agents/audit.md +18 -18
- package/ansible/agents/agents.yml +134 -0
- package/ansible/agents/templates/antigravity.settings.json.j2 +3 -0
- package/ansible/agents/templates/claude.settings.json.j2 +3 -0
- package/ansible/agents/templates/codex.config.toml.j2 +11 -0
- package/ansible/agents/templates/gemini.settings.json.j2 +14 -0
- package/ansible/agents/templates/grok.config.toml.j2 +19 -0
- package/ansible/agents/templates/opencode.json.j2 +24 -0
- package/ansible/agents/templates/receipt.json.j2 +10 -0
- package/ansible/ansible.cfg +14 -0
- package/ansible/github/git.yml +82 -0
- package/ansible/macos/manager-privilege.yml +83 -0
- package/ansible/macos/manager.yml +164 -9
- package/ansible/macos/policy.json +13 -0
- package/ansible/macos/t3-policy.yml +5 -3
- package/ansible/macos/vault-renew.yml +85 -0
- package/ansible/macos/worker-host-key.yml +33 -0
- package/ansible/monitoring/agent-darwin.yml +3 -0
- package/ansible/monitoring/agent.yml +2 -0
- package/ansible/monitoring/hub-darwin.yml +3 -0
- package/ansible/monitoring/hub-route-darwin.yml +1 -0
- package/ansible/net/remote-probe.sh +4 -1
- package/ansible/platforms.json +2 -2
- package/ansible/pyproject.toml +5 -0
- package/ansible/ubuntu/baseline.yml +39 -13
- package/ansible/ubuntu/disk-gc.py +6 -2
- package/ansible/ubuntu/firewall.nft.j2 +50 -0
- package/ansible/ubuntu/policy.json +125 -6
- package/ansible/ubuntu/reboot.yml +89 -0
- package/ansible/ubuntu/recovery.yml +13 -3
- package/ansible/ubuntu/t3/policy.json +3 -0
- package/ansible/ubuntu/t3-desktop.yml +23 -2
- package/ansible/ubuntu/t3-headless.yml +128 -0
- package/ansible/ubuntu/t3-health.sh +24 -10
- package/ansible/ubuntu/t3-idle.py +37 -2
- package/ansible/ubuntu/t3-policy.yml +4 -1
- package/ansible/ubuntu/t3-route.yml +1 -0
- package/ansible/ubuntu/tailscale.yml +54 -8
- package/ansible/ubuntu/tasks/browser.yml +3 -0
- package/ansible/ubuntu/tasks/containers-off.yml +43 -0
- package/ansible/ubuntu/tasks/docker.yml +53 -6
- package/ansible/ubuntu/tasks/dpkg.yml +4 -0
- package/ansible/ubuntu/tasks/features.yml +22 -0
- package/ansible/ubuntu/tasks/firewall.yml +172 -0
- package/ansible/ubuntu/tasks/install.yml +2 -1
- package/ansible/ubuntu/tasks/packages.yml +4 -2
- package/ansible/ubuntu/tasks/release.yml +6 -0
- package/ansible/ubuntu/tasks/resources.yml +6 -4
- package/ansible/ubuntu/tasks/services.yml +2 -2
- package/ansible/ubuntu/tasks/sizing.yml +49 -0
- package/ansible/ubuntu/tasks/snapshot.yml +14 -3
- package/ansible/ubuntu/tasks/ssh.yml +61 -4
- package/ansible/ubuntu/tasks/t3-restart.yml +3 -1
- package/ansible/ubuntu/tasks/user.yml +1 -0
- package/ansible/ubuntu/worker-account.yml +117 -0
- package/ansible/uv.lock +265 -0
- package/bin/devfleet.mjs +42 -4
- package/bootstrap/fleet-remote-bootstrap.sh +91 -25
- package/bootstrap/global-config.ts +29 -3
- package/bootstrap/package-smoke.ts +127 -0
- package/bootstrap/pins.ts +189 -0
- package/bootstrap/release-notes.ts +88 -0
- package/bootstrap/release-version.ts +41 -7
- package/bootstrap/worker-features.ts +114 -0
- package/bootstrap/worker-seed.yaml +65 -0
- package/bootstrap/worker-tools.ts +39 -9
- package/cliproxy/README.md +1 -1
- package/cliproxy/docs/health-workflow.example.yml +20 -143
- package/docs/authentication.md +21 -11
- package/docs/consumer-setup.md +74 -58
- package/docs/convergence.md +67 -0
- package/docs/declared.md +61 -34
- package/docs/machine-setup.md +73 -7
- package/docs/manager-setup.md +34 -0
- package/docs/operations.md +64 -9
- package/docs/releasing.md +49 -1
- package/docs/runbooks.md +112 -7
- package/docs/state.md +23 -11
- package/docs/support-matrix.md +40 -4
- package/docs/topology.md +82 -12
- package/docs/ubuntu-sync.md +98 -42
- package/docs/worker-contract.md +122 -19
- package/docs/worker-requirements.md +107 -0
- package/examples/fleet.json +1 -0
- package/examples/workers-from-infrastructure/main.tf +132 -0
- package/examples/workers-from-infrastructure/worker-records.json +14 -0
- package/llms.txt +3 -1
- package/mise.lock +29 -0
- package/mise.toml +5 -3
- package/package.json +2 -2
- package/schema/check-ids.json +1991 -162
- package/schema/check-ids.schema.json +34 -0
- package/schema/fleet.schema.json +349 -10
- package/schema/output/check.schema.json +1 -1
- package/schema/output/fleet-state-definition.schema.json +14 -0
- package/schema/output/fleet-sync-schedule.schema.json +26 -0
- package/schema/output/fleet-sync.schema.json +18 -0
- package/schema/output/report.schema.json +3 -2
- package/schema/worker-record.schema.json +76 -0
- package/src/agents/converge.ts +140 -24
- package/src/agents/files.ts +2 -32
- package/src/agents/native-verify.ts +34 -3
- package/src/agents/secrets.ts +39 -4
- package/src/check-owners.ts +62 -2
- package/src/check-registry.ts +630 -0
- package/src/cli/agent-guide.ts +3 -3
- package/src/cli/cli.ts +4 -0
- package/src/cli/fleet-manifest.ts +108 -0
- package/src/cli/fleet-node-lifecycle.ts +704 -0
- package/src/cli/fleet-node-park.ts +238 -0
- package/src/cli/fleet-node-seed.ts +167 -0
- package/src/cli/fleet-state.ts +2 -1
- package/src/cli/fleet-sync-checkout.ts +7 -0
- package/src/cli/fleet-sync-enrollment.ts +193 -32
- package/src/cli/fleet-sync-forward.ts +6 -0
- package/src/cli/fleet-sync-manager.ts +123 -8
- package/src/cli/fleet-sync-output.ts +31 -11
- package/src/cli/fleet-sync-plan.ts +65 -9
- package/src/cli/fleet-sync-records.ts +113 -0
- package/src/cli/fleet-sync-remote.ts +172 -15
- package/src/cli/fleet-sync-schedule.ts +14 -4
- package/src/cli/fleet-sync-state.ts +31 -10
- package/src/cli/fleet-sync-vault.ts +152 -26
- package/src/cli/fleet-sync-worker.ts +214 -0
- package/src/cli/fleet-sync.ts +172 -21
- package/src/cli/fleet-validate.ts +277 -0
- package/src/cli/fleet-watch.ts +7 -1
- package/src/cli/fleet.ts +87 -5
- package/src/cli/managed-health.ts +25 -1
- package/src/cli/register-fleet.ts +251 -4
- package/src/cli/registry.ts +47 -0
- package/src/cli/tailnet.ts +43 -10
- package/src/cli/ui-data.ts +3 -1
- package/src/cli/worker-usage-report.ts +63 -0
- package/src/clock.ts +42 -0
- package/src/definition.ts +762 -232
- package/src/diagnostics.ts +5 -2
- package/src/exposure.ts +102 -0
- package/src/fleet-config.ts +3 -1
- package/src/git-identity.ts +76 -3
- package/src/ide/ide.ts +5 -0
- package/src/ide/t3.ts +12 -2
- package/src/input-hashes.ts +6 -0
- package/src/machine/machine-class.ts +59 -0
- package/src/machine/platform.ts +90 -11
- package/src/machine/state.ts +70 -16
- package/src/machine/support-matrix.ts +11 -9
- package/src/machine-health.ts +28 -5
- package/src/manager-health.ts +148 -2
- package/src/manager-sync.ts +89 -8
- package/src/monitoring/beszel.ts +8 -8
- package/src/monitoring/enroll.ts +8 -1
- package/src/monitoring/hub-access.ts +10 -4
- package/src/monitoring/systems.ts +138 -0
- package/src/network-definition.ts +13 -4
- package/src/node-converge-linux.ts +136 -27
- package/src/node-converge.ts +13 -3
- package/src/platform/ansible-receipt.ts +17 -2
- package/src/platform/ansible.ts +40 -4
- package/src/platform/manager-host.ts +25 -8
- package/src/platform/resource-verify.ts +18 -7
- package/src/platform/t3-policy.ts +24 -10
- package/src/platform/t3-service.ts +109 -34
- package/src/platform/ubuntu-release.ts +24 -0
- package/src/platform/ubuntu-verify.ts +31 -3
- package/src/platform/ubuntu-worker.ts +58 -0
- package/src/platform/worker-platform.ts +72 -0
- package/src/protocol.ts +1 -1
- package/src/public-reachability.ts +186 -0
- package/src/remote-bootstrap.ts +50 -12
- package/src/remote-operation.ts +18 -4
- package/src/repair-plan.ts +23 -12
- package/src/report.ts +2 -1
- package/src/schedule-check.ts +21 -2
- package/src/secrets.ts +7 -0
- package/src/service-auth.ts +55 -48
- package/src/ssh-connection.ts +141 -0
- package/src/ssh.ts +63 -24
- package/src/state/store.ts +4 -2
- package/src/state/sync.ts +35 -6
- package/src/state/workers.ts +140 -0
- package/src/t3/pairing.ts +59 -0
- package/src/t3/peer-link.ts +333 -0
- package/src/t3/version.ts +1 -1
- package/src/tailnet-cleanup.ts +185 -0
- package/src/tailnet-control.ts +346 -0
- package/src/tailscale.ts +1 -1
- package/src/toolchain-checks.ts +11 -1
- package/src/types.ts +4 -2
- package/src/worker-account.ts +63 -0
- package/src/worker-activity.ts +303 -0
- package/src/worker-capacity.ts +31 -0
- package/src/worker-credentials.ts +201 -0
- package/src/worker-exposure.ts +388 -0
- package/src/worker-features.ts +201 -0
- package/src/worker-host-checks.ts +71 -20
- package/src/worker-manifest.ts +224 -0
- package/src/worker-parking.ts +70 -0
- package/src/worker-policy.ts +38 -36
- package/src/worker-readiness.ts +15 -3
- package/src/worker-reboot.ts +129 -0
- package/src/worker-records.ts +305 -0
- package/src/worker-seed.ts +143 -0
- package/src/worker-sizing.ts +76 -0
- package/src/cli/schedule-logs.ts +0 -36
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
---
|
|
2
|
+
# The manager's own policy, applied by fleet-sync on every applying run:
|
|
3
|
+
# check mode first, then for real, with a receipt (macos-manager) that a full
|
|
4
|
+
# re-apply replaces at least once a day. It never asks for a password and no
|
|
5
|
+
# play becomes root: what needs root runs through the narrow grant
|
|
6
|
+
# manager-privilege.yml installs once (with the T3 policy and the log
|
|
7
|
+
# rotation), and without the grant it is skipped here and reported by its
|
|
8
|
+
# check.
|
|
9
|
+
#
|
|
2
10
|
# Manager power policy. Workers depend on the manager's singleton services
|
|
3
11
|
# (CLI proxy, Beszel hub), so the manager never sleeps on AC power and starts
|
|
4
12
|
# again by itself when power returns (autorestart). Sleep on battery is left
|
|
5
|
-
# to the human. The settings are the `pmset` object in
|
|
6
|
-
#
|
|
7
|
-
# them needs an administrator: ansible-playbook -K manager.yml
|
|
13
|
+
# to the human. The settings are the `pmset` object in policy.json; fleet-sync
|
|
14
|
+
# measures the same object as manager:power.
|
|
8
15
|
- name: Keep the fleet manager awake on AC power
|
|
9
16
|
hosts: localhost
|
|
10
17
|
connection: local
|
|
11
|
-
become: true
|
|
12
18
|
gather_facts: false
|
|
13
19
|
vars_files:
|
|
14
20
|
- policy.json
|
|
@@ -17,17 +23,52 @@
|
|
|
17
23
|
ansible.builtin.command:
|
|
18
24
|
argv: [/usr/bin/pmset, -g, custom]
|
|
19
25
|
changed_when: false
|
|
26
|
+
check_mode: false
|
|
20
27
|
register: pmset_custom
|
|
28
|
+
# Lists, never runs: whether `pmset -c` may run without a password.
|
|
29
|
+
- name: Inspect the manager's power grant
|
|
30
|
+
ansible.builtin.command:
|
|
31
|
+
argv: [/usr/bin/sudo, -n, -l, /usr/bin/pmset, -c, sleep, '0']
|
|
32
|
+
changed_when: false
|
|
33
|
+
check_mode: false
|
|
34
|
+
failed_when: false
|
|
35
|
+
register: pmset_grant
|
|
36
|
+
- name: Report that the power settings wait for the grant
|
|
37
|
+
ansible.builtin.debug:
|
|
38
|
+
msg: 'The power grant is not in place; manager:power says how to give it once.'
|
|
39
|
+
when: pmset_grant.rc != 0
|
|
40
|
+
# pmset accepts a setting this Mac does not have and keeps nothing (a
|
|
41
|
+
# virtual Mac has no Wake-on-LAN, so no womp): applying it again on every
|
|
42
|
+
# run would never converge. It is reported instead of applied.
|
|
43
|
+
- name: Report the declared AC power settings this Mac does not have
|
|
44
|
+
ansible.builtin.debug:
|
|
45
|
+
msg: 'pmset on this Mac has no AC setting {{ item.key }}; it is not applied.'
|
|
46
|
+
loop: '{{ pmset | dict2items }}'
|
|
47
|
+
loop_control:
|
|
48
|
+
label: '{{ item.key }}'
|
|
49
|
+
vars:
|
|
50
|
+
ac_section: "{{ pmset_custom.stdout.split('AC Power:') | last }}"
|
|
51
|
+
when: ac_section is not search('^\\s*' ~ item.key ~ '\\s', multiline=True)
|
|
21
52
|
- name: Apply the declared AC power settings
|
|
22
53
|
ansible.builtin.command:
|
|
23
|
-
argv:
|
|
54
|
+
argv:
|
|
55
|
+
[
|
|
56
|
+
/usr/bin/sudo,
|
|
57
|
+
-n,
|
|
58
|
+
/usr/bin/pmset,
|
|
59
|
+
-c,
|
|
60
|
+
'{{ item.key }}',
|
|
61
|
+
'{{ item.value }}',
|
|
62
|
+
]
|
|
24
63
|
loop: '{{ pmset | dict2items }}'
|
|
25
64
|
loop_control:
|
|
26
65
|
label: '{{ item.key }}'
|
|
27
66
|
vars:
|
|
28
67
|
ac_section: "{{ pmset_custom.stdout.split('AC Power:') | last }}"
|
|
29
68
|
observed: "{{ ac_section | regex_findall('^\\s*' ~ item.key ~ '\\s+(\\S+)', multiline=True) | first | default('') }}"
|
|
30
|
-
when:
|
|
69
|
+
when:
|
|
70
|
+
- pmset_grant.rc == 0
|
|
71
|
+
- observed not in ['', item.value]
|
|
31
72
|
changed_when: true
|
|
32
73
|
|
|
33
74
|
# Docker Desktop hosts the CLI proxy and the monitoring hub, so it starts at
|
|
@@ -64,7 +105,121 @@
|
|
|
64
105
|
# Docker Desktop writes the file the first time it runs.
|
|
65
106
|
when:
|
|
66
107
|
- docker_settings_file.stat.exists
|
|
67
|
-
- (current[dockerDesktop.key] | default(false)
|
|
108
|
+
- not (current[dockerDesktop.key] | default(false) | bool)
|
|
68
109
|
|
|
69
|
-
-
|
|
70
|
-
|
|
110
|
+
# fleet-watch compares the fleet's health with its last run, notifies on a
|
|
111
|
+
# change and stores the heartbeat the off-machine workflow
|
|
112
|
+
# (.github/workflows/fleet-heartbeat.yml) reads. It runs from the checkout
|
|
113
|
+
# fleet-sync last applied (fleet/current) with that checkout's locked Bun, on
|
|
114
|
+
# the interval `manager.schedule.watchMinutes` in fleet.json sets
|
|
115
|
+
# (fleet_watch_minutes), or else policy.json's. manager:watch measures it.
|
|
116
|
+
- name: Watch the fleet on a schedule
|
|
117
|
+
hosts: localhost
|
|
118
|
+
connection: local
|
|
119
|
+
become: false
|
|
120
|
+
gather_facts: true
|
|
121
|
+
gather_subset: ['!all']
|
|
122
|
+
vars_files:
|
|
123
|
+
- policy.json
|
|
124
|
+
vars:
|
|
125
|
+
watch_home: '{{ ansible_facts.env.HOME }}'
|
|
126
|
+
watch_label: '{{ fleetWatch.label }}'
|
|
127
|
+
watch_minutes: '{{ fleet_watch_minutes | default(fleetWatch.intervalMinutes) | int }}'
|
|
128
|
+
watch_logs: '{{ watch_home }}/{{ jobLogs.directory }}'
|
|
129
|
+
watch_plist: '{{ watch_home }}/Library/LaunchAgents/{{ watch_label }}.plist'
|
|
130
|
+
watch_domain: 'gui/{{ ansible_facts.user_uid }}'
|
|
131
|
+
tasks:
|
|
132
|
+
- name: Require an interval the schema allows
|
|
133
|
+
ansible.builtin.assert:
|
|
134
|
+
that:
|
|
135
|
+
- watch_minutes | int >= 5
|
|
136
|
+
- watch_minutes | int <= 1440
|
|
137
|
+
fail_msg: MANAGER_WATCH_INTERVAL_INVALID; manager.schedule.watchMinutes is from 5 to 1440.
|
|
138
|
+
- name: Create the directories of the watch job
|
|
139
|
+
ansible.builtin.file:
|
|
140
|
+
path: '{{ item }}'
|
|
141
|
+
state: directory
|
|
142
|
+
mode: '0700'
|
|
143
|
+
loop:
|
|
144
|
+
- '{{ watch_logs }}'
|
|
145
|
+
- '{{ watch_home }}/Library/LaunchAgents'
|
|
146
|
+
- name: Install the fleet-watch launch agent
|
|
147
|
+
ansible.builtin.copy:
|
|
148
|
+
dest: '{{ watch_plist }}'
|
|
149
|
+
mode: '0644'
|
|
150
|
+
content: |
|
|
151
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
152
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
153
|
+
<!-- Managed by dev-fleet: fleet-watch -->
|
|
154
|
+
<plist version="1.0"><dict>
|
|
155
|
+
<key>Label</key><string>{{ watch_label }}</string>
|
|
156
|
+
<key>ProgramArguments</key><array>
|
|
157
|
+
<string>{{ watch_home }}/.local/bin/mise</string>
|
|
158
|
+
<string>exec</string>
|
|
159
|
+
<string>--locked</string>
|
|
160
|
+
<string>bun</string>
|
|
161
|
+
<string>--</string>
|
|
162
|
+
<string>bun</string>
|
|
163
|
+
<string>src/cli/cli.ts</string>
|
|
164
|
+
<string>fleet-watch</string>
|
|
165
|
+
<string>--json</string>
|
|
166
|
+
</array>
|
|
167
|
+
<key>WorkingDirectory</key><string>{{ watch_home }}/.local/share/personal/fleet/current</string>
|
|
168
|
+
<key>EnvironmentVariables</key><dict>
|
|
169
|
+
<key>PATH</key><string>{{ watch_home }}/.local/share/mise/shims:{{ watch_home }}/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
170
|
+
<key>DEVFLEET_TRIGGER</key><string>watch</string>
|
|
171
|
+
</dict>
|
|
172
|
+
<key>StartInterval</key><integer>{{ watch_minutes | int * 60 }}</integer>
|
|
173
|
+
<key>ProcessType</key><string>Background</string>
|
|
174
|
+
<key>LowPriorityIO</key><true/>
|
|
175
|
+
<key>Nice</key><integer>10</integer>
|
|
176
|
+
<key>Umask</key><integer>63</integer>
|
|
177
|
+
<key>StandardOutPath</key><string>{{ watch_logs }}/{{ watch_label }}.out.log</string>
|
|
178
|
+
<key>StandardErrorPath</key><string>{{ watch_logs }}/{{ watch_label }}.err.log</string>
|
|
179
|
+
</dict></plist>
|
|
180
|
+
register: watch_agent
|
|
181
|
+
- name: Inspect whether the watch job is loaded
|
|
182
|
+
ansible.builtin.command:
|
|
183
|
+
argv: [/bin/launchctl, print, '{{ watch_domain }}/{{ watch_label }}']
|
|
184
|
+
changed_when: false
|
|
185
|
+
check_mode: false
|
|
186
|
+
failed_when: false
|
|
187
|
+
register: watch_loaded
|
|
188
|
+
# launchd caches a loaded definition: a changed plist needs a reload.
|
|
189
|
+
- name: Unload a stale watch job definition
|
|
190
|
+
ansible.builtin.command:
|
|
191
|
+
argv: [/bin/launchctl, bootout, '{{ watch_domain }}/{{ watch_label }}']
|
|
192
|
+
when: watch_agent.changed and watch_loaded.rc == 0
|
|
193
|
+
failed_when: false
|
|
194
|
+
changed_when: true
|
|
195
|
+
- name: Load the watch job
|
|
196
|
+
ansible.builtin.command:
|
|
197
|
+
argv:
|
|
198
|
+
[/bin/launchctl, bootstrap, '{{ watch_domain }}', '{{ watch_plist }}']
|
|
199
|
+
when: watch_agent.changed or watch_loaded.rc != 0
|
|
200
|
+
changed_when: true
|
|
201
|
+
|
|
202
|
+
# A skewed clock breaks TLS, token validation and Vault logins with errors
|
|
203
|
+
# that never mention time, and every worker's offset is measured against the
|
|
204
|
+
# manager's. macOS keeps its own time service; this asserts it is on.
|
|
205
|
+
# Reading it needs root, through the grant manager-privilege.yml installs;
|
|
206
|
+
# without the grant it is not asserted here, and manager:clock says so.
|
|
207
|
+
- name: Keep the manager's clock on network time
|
|
208
|
+
hosts: localhost
|
|
209
|
+
connection: local
|
|
210
|
+
become: false
|
|
211
|
+
gather_facts: false
|
|
212
|
+
tasks:
|
|
213
|
+
- name: Read whether network time is on
|
|
214
|
+
ansible.builtin.command:
|
|
215
|
+
argv: [/usr/bin/sudo, -n, /usr/sbin/systemsetup, -getusingnetworktime]
|
|
216
|
+
changed_when: false
|
|
217
|
+
check_mode: false
|
|
218
|
+
failed_when: false
|
|
219
|
+
register: network_time
|
|
220
|
+
- name: Require network time
|
|
221
|
+
ansible.builtin.assert:
|
|
222
|
+
that:
|
|
223
|
+
- "network_time.stdout is search('Network Time:\\s*On')"
|
|
224
|
+
fail_msg: MANAGER_CLOCK_NOT_SYNCHRONISED; turn on Set time and date automatically in System Settings > General > Date & Time.
|
|
225
|
+
when: network_time.rc == 0
|
|
@@ -21,5 +21,18 @@
|
|
|
21
21
|
"dockerDesktop": {
|
|
22
22
|
"settings": "Library/Group Containers/group.com.docker/settings-store.json",
|
|
23
23
|
"key": "AutoStart"
|
|
24
|
+
},
|
|
25
|
+
"fleetSync": {
|
|
26
|
+
"label": "personal-fleet-sync",
|
|
27
|
+
"intervalMinutes": 30
|
|
28
|
+
},
|
|
29
|
+
"fleetWatch": {
|
|
30
|
+
"label": "dev.devfleet.fleet-watch",
|
|
31
|
+
"intervalMinutes": 15
|
|
32
|
+
},
|
|
33
|
+
"jobLogs": {
|
|
34
|
+
"directory": ".local/share/personal/fleet/manager/logs",
|
|
35
|
+
"maxKiB": 1024,
|
|
36
|
+
"keep": 3
|
|
24
37
|
}
|
|
25
38
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
# T3 Code on the manager reads the same administrator policy as on every
|
|
3
|
-
# worker (ansible/ubuntu/t3/policy.json):
|
|
4
|
-
#
|
|
3
|
+
# worker (ansible/ubuntu/t3/policy.json): linked environments on, so the
|
|
4
|
+
# manager pairs with a replaced worker's T3 by itself, and updates off, so
|
|
5
|
+
# the manager's T3 changes only when the fleet's pin does. Provider instances are reconciled
|
|
5
6
|
# by fleet-sync on every run and stay unlocked here: locking them needs an
|
|
6
7
|
# administrator for each change of the account roster. fleet-sync measures
|
|
7
|
-
# the result as t3:updates. Needs an administrator
|
|
8
|
+
# the result as t3:updates. Needs an administrator, once, through
|
|
9
|
+
# manager-privilege.yml: ansible-playbook -K manager-privilege.yml -e user=$(id -un)
|
|
8
10
|
- name: Stage the fleet's T3 policy on the manager
|
|
9
11
|
hosts: localhost
|
|
10
12
|
connection: local
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
# The manager's own Vault session renews itself, as every worker's does
|
|
3
|
+
# (personal-worker-auth): without it the session lapses while nobody runs
|
|
4
|
+
# fleet-sync, and the manager can no longer read the secrets its services and
|
|
5
|
+
# its agents use. The job only renews; a lapsed session needs one login by a
|
|
6
|
+
# person. Measured as manager:vault-renewal. fleet-sync passes vault_address
|
|
7
|
+
# (the fleet configuration's secrets block) and mise (its absolute path).
|
|
8
|
+
- name: Keep the manager's Vault session renewed
|
|
9
|
+
hosts: localhost
|
|
10
|
+
connection: local
|
|
11
|
+
become: false
|
|
12
|
+
gather_facts: true
|
|
13
|
+
vars:
|
|
14
|
+
renew_label: dev.devfleet.vault-renew
|
|
15
|
+
renew_plist: '{{ ansible_facts.env.HOME }}/Library/LaunchAgents/{{ renew_label }}.plist'
|
|
16
|
+
renew_domain: 'gui/{{ ansible_facts.user_uid }}'
|
|
17
|
+
tasks:
|
|
18
|
+
- name: Require the macOS manager and an HTTPS Vault
|
|
19
|
+
ansible.builtin.assert:
|
|
20
|
+
that:
|
|
21
|
+
- ansible_facts.os_family == 'Darwin'
|
|
22
|
+
- vault_address is match('^https://[A-Za-z0-9.-]+$')
|
|
23
|
+
- mise is match('^/')
|
|
24
|
+
fail_msg: VAULT_RENEWAL_NOT_MANAGER; the renewal job is for the macOS manager's own session.
|
|
25
|
+
- name: Create the launch agents directory
|
|
26
|
+
ansible.builtin.file:
|
|
27
|
+
path: '{{ ansible_facts.env.HOME }}/Library/LaunchAgents'
|
|
28
|
+
state: directory
|
|
29
|
+
mode: '0700'
|
|
30
|
+
# Every ten minutes, like the workers, and at load: a renewal never
|
|
31
|
+
# outlives the token, so a missed run only shortens the margin. Output is
|
|
32
|
+
# dropped (the lease, or why it failed): manager:vault-renewal measures the
|
|
33
|
+
# session itself.
|
|
34
|
+
- name: Install the renewal job's launch agent
|
|
35
|
+
ansible.builtin.copy:
|
|
36
|
+
dest: '{{ renew_plist }}'
|
|
37
|
+
mode: '0644'
|
|
38
|
+
content: |
|
|
39
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
40
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
41
|
+
<!-- Managed by personal Fleet: Vault session renewal -->
|
|
42
|
+
<plist version="1.0"><dict>
|
|
43
|
+
<key>Label</key><string>{{ renew_label }}</string>
|
|
44
|
+
<key>ProgramArguments</key><array>
|
|
45
|
+
<string>{{ mise }}</string>
|
|
46
|
+
<string>exec</string>
|
|
47
|
+
<string>--</string>
|
|
48
|
+
<string>vault</string>
|
|
49
|
+
<string>token</string>
|
|
50
|
+
<string>renew</string>
|
|
51
|
+
<string>-address={{ vault_address }}</string>
|
|
52
|
+
<string>-increment=24h</string>
|
|
53
|
+
</array>
|
|
54
|
+
<key>WorkingDirectory</key><string>{{ ansible_facts.env.HOME }}</string>
|
|
55
|
+
<key>EnvironmentVariables</key><dict>
|
|
56
|
+
<key>PATH</key><string>{{ mise | dirname }}:/usr/local/bin:/usr/bin:/bin</string>
|
|
57
|
+
</dict>
|
|
58
|
+
<key>RunAtLoad</key><true/>
|
|
59
|
+
<key>StartInterval</key><integer>600</integer>
|
|
60
|
+
<key>ProcessType</key><string>Background</string>
|
|
61
|
+
<key>Umask</key><integer>63</integer>
|
|
62
|
+
<key>StandardOutPath</key><string>/dev/null</string>
|
|
63
|
+
<key>StandardErrorPath</key><string>/dev/null</string>
|
|
64
|
+
</dict></plist>
|
|
65
|
+
register: renew_agent
|
|
66
|
+
- name: Inspect whether the renewal job is loaded
|
|
67
|
+
ansible.builtin.command:
|
|
68
|
+
argv: [/bin/launchctl, print, '{{ renew_domain }}/{{ renew_label }}']
|
|
69
|
+
changed_when: false
|
|
70
|
+
check_mode: false
|
|
71
|
+
failed_when: false
|
|
72
|
+
register: renew_loaded
|
|
73
|
+
# launchd caches a loaded definition: a changed plist needs a reload.
|
|
74
|
+
- name: Unload a stale renewal job definition
|
|
75
|
+
ansible.builtin.command:
|
|
76
|
+
argv: [/bin/launchctl, bootout, '{{ renew_domain }}/{{ renew_label }}']
|
|
77
|
+
when: renew_agent.changed and renew_loaded.rc == 0
|
|
78
|
+
failed_when: false
|
|
79
|
+
changed_when: true
|
|
80
|
+
- name: Load the renewal job
|
|
81
|
+
ansible.builtin.command:
|
|
82
|
+
argv:
|
|
83
|
+
[/bin/launchctl, bootstrap, '{{ renew_domain }}', '{{ renew_plist }}']
|
|
84
|
+
when: renew_agent.changed or renew_loaded.rc != 0
|
|
85
|
+
changed_when: true
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
# A worker's pinned SSH host key in the manager's known_hosts. fleet-sync
|
|
3
|
+
# connects with StrictHostKeyChecking=yes, so this is the only way a key is
|
|
4
|
+
# trusted: devfleet fleet-node-replace pins the key of a replacement worker
|
|
5
|
+
# (given by the operator, or by the seed that created it with
|
|
6
|
+
# fleet-node-seed) and devfleet fleet-node-retire forgets it. A key is never
|
|
7
|
+
# learned on first use. Variables: host (the name ssh connects to, with
|
|
8
|
+
# [host]:port for another port), pin (present or absent), key (with present:
|
|
9
|
+
# "TYPE BASE64") and known_hosts (the file).
|
|
10
|
+
- name: Pin or forget a worker's SSH host key on the manager
|
|
11
|
+
hosts: localhost
|
|
12
|
+
connection: local
|
|
13
|
+
gather_facts: false
|
|
14
|
+
tasks:
|
|
15
|
+
- name: Check the request
|
|
16
|
+
ansible.builtin.assert:
|
|
17
|
+
that:
|
|
18
|
+
- "host is match('^\\[?[a-zA-Z0-9][a-zA-Z0-9._-]{0,252}(\\]:[0-9]{1,5})?$')"
|
|
19
|
+
- "pin in ['present', 'absent']"
|
|
20
|
+
- "pin == 'absent' or (key is match('^(ssh-ed25519|ecdsa-sha2-nistp256|ssh-rsa) [A-Za-z0-9+/=]{16,}$'))"
|
|
21
|
+
- name: Pin the worker's host key
|
|
22
|
+
ansible.builtin.known_hosts:
|
|
23
|
+
name: '{{ host }}'
|
|
24
|
+
key: '{{ host }} {{ key }}'
|
|
25
|
+
path: '{{ known_hosts }}'
|
|
26
|
+
state: present
|
|
27
|
+
when: pin == 'present'
|
|
28
|
+
- name: Forget every key of the retired worker
|
|
29
|
+
ansible.builtin.known_hosts:
|
|
30
|
+
name: '{{ host }}'
|
|
31
|
+
path: '{{ known_hosts }}'
|
|
32
|
+
state: absent
|
|
33
|
+
when: pin == 'absent'
|
|
@@ -137,6 +137,7 @@
|
|
|
137
137
|
ansible.builtin.command:
|
|
138
138
|
argv: [/bin/launchctl, print, '{{ beszel_domain }}/{{ beszelDaemon }}']
|
|
139
139
|
changed_when: false
|
|
140
|
+
check_mode: false
|
|
140
141
|
failed_when: false
|
|
141
142
|
register: agent_loaded
|
|
142
143
|
- name: Load the agent at login and now
|
|
@@ -156,6 +157,8 @@
|
|
|
156
157
|
ansible.builtin.command:
|
|
157
158
|
argv: [/bin/launchctl, print, '{{ beszel_domain }}/{{ beszelDaemon }}']
|
|
158
159
|
changed_when: false
|
|
160
|
+
# Verifies a change a dry run does not make.
|
|
161
|
+
when: not ansible_check_mode
|
|
159
162
|
register: agent_state
|
|
160
163
|
retries: 5
|
|
161
164
|
delay: 2
|
|
@@ -166,6 +166,8 @@
|
|
|
166
166
|
ansible.builtin.command:
|
|
167
167
|
argv: [systemctl, is-active, beszel-agent.service]
|
|
168
168
|
changed_when: false
|
|
169
|
+
# Verifies a change a dry run does not make.
|
|
170
|
+
when: not ansible_check_mode
|
|
169
171
|
handlers:
|
|
170
172
|
- name: Restart agent
|
|
171
173
|
ansible.builtin.systemd_service:
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
ansible.builtin.command:
|
|
40
40
|
argv: [docker, compose, version]
|
|
41
41
|
changed_when: false
|
|
42
|
+
check_mode: false
|
|
42
43
|
- name: Inspect an existing hub image before any upgrade
|
|
43
44
|
ansible.builtin.command:
|
|
44
45
|
argv:
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
personal-beszel-hub-1,
|
|
51
52
|
]
|
|
52
53
|
changed_when: false
|
|
54
|
+
check_mode: false
|
|
53
55
|
failed_when: false
|
|
54
56
|
register: existing_hub
|
|
55
57
|
- name: Require explicit maintenance for a different existing image
|
|
@@ -221,6 +223,7 @@
|
|
|
221
223
|
argv:
|
|
222
224
|
[/bin/launchctl, print, '{{ watchdog_domain }}/{{ watchdog_label }}']
|
|
223
225
|
changed_when: false
|
|
226
|
+
check_mode: false
|
|
224
227
|
failed_when: false
|
|
225
228
|
register: watchdog_loaded
|
|
226
229
|
# launchd caches a loaded definition: a changed plist needs a reload.
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
argv: ['{{ beszel_tailscale_cli }}', serve, status, --json]
|
|
30
30
|
register: beszel_serve_status
|
|
31
31
|
changed_when: false
|
|
32
|
+
check_mode: false
|
|
32
33
|
- name: Publish the hub on the tailnet HTTPS port as the signed-in user
|
|
33
34
|
ansible.builtin.command:
|
|
34
35
|
argv: '{{ beszel_serve }}'
|
|
@@ -12,7 +12,10 @@ else
|
|
|
12
12
|
df -Pi "$HOME" | awk 'NR==2 && $2>0{print "inodefree=" int($4*100/$2)}'
|
|
13
13
|
# Clock synchronisation, as systemd sees it: 1 or 0.
|
|
14
14
|
timedatectl show -p NTPSynchronized -p NTP 2>/dev/null | awk -F= '{v=($2=="yes")?1:0} $1=="NTP"{print "ntp=" v} $1=="NTPSynchronized"{print "ntpsync=" v}'
|
|
15
|
-
|
|
15
|
+
# Agent sessions running now: the scopes T3 starts its agents in.
|
|
16
|
+
echo "agentsessions=$(systemctl --user list-units --type=scope --state=active --no-legend 't3code-*.scope' 2>/dev/null | grep -c .)"
|
|
17
|
+
awk '/MemTotal/{t=$2} /MemAvailable/{a=$2} /SwapTotal/{st=$2} /SwapFree/{sf=$2} END{print "memfree=" int(a*100/t); print "memtotalmb=" int(t/1024); if (st>0) print "swap=" int((st-sf)*100/st); else print "swap=0"}' /proc/meminfo
|
|
16
18
|
fi
|
|
17
19
|
echo "diskmb=$(df -Pk "$HOME" | awk 'NR==2{print int($4/1024)}')"
|
|
20
|
+
echo "disktotalmb=$(df -Pk "$HOME" | awk 'NR==2{print int($2/1024)}')"
|
|
18
21
|
echo "epoch=$(date +%s)"
|
package/ansible/platforms.json
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"versions": "any",
|
|
32
32
|
"architectures": "any",
|
|
33
33
|
"status": "refused",
|
|
34
|
-
"note": "UBUNTU_BASELINE_MISMATCH"
|
|
34
|
+
"note": "UBUNTU_BASELINE_MISMATCH; not yet supported (see below)."
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"role": "worker",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"versions": "any",
|
|
40
40
|
"architectures": "any",
|
|
41
41
|
"status": "refused",
|
|
42
|
-
"note": "The fleet configuration accepts only Linux workers."
|
|
42
|
+
"note": "The fleet configuration accepts only Linux workers; not yet supported (see below)."
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"role": "worker",
|
package/ansible/pyproject.toml
CHANGED
|
@@ -10,3 +10,8 @@ dependencies = ["ansible-core==2.19.9"]
|
|
|
10
10
|
|
|
11
11
|
[tool.uv]
|
|
12
12
|
package = false
|
|
13
|
+
|
|
14
|
+
# The linter CI runs on every playbook (`uv run --locked --group lint
|
|
15
|
+
# ansible-lint`). A separate group: the manager never installs it.
|
|
16
|
+
[dependency-groups]
|
|
17
|
+
lint = ["ansible-lint==26.11.0"]
|
|
@@ -7,8 +7,13 @@
|
|
|
7
7
|
vars_files:
|
|
8
8
|
- policy.json
|
|
9
9
|
pre_tasks:
|
|
10
|
-
-
|
|
10
|
+
- name: Require a supported Ubuntu release
|
|
11
|
+
ansible.builtin.import_tasks: tasks/release.yml
|
|
11
12
|
tags: [always]
|
|
13
|
+
- name: Require the minimum worker size and size the disk and memory lines
|
|
14
|
+
ansible.builtin.import_tasks: tasks/sizing.yml
|
|
15
|
+
- name: Leave out what the features this worker lacks would install
|
|
16
|
+
ansible.builtin.import_tasks: tasks/features.yml
|
|
12
17
|
- name: Require an explicit non-root user
|
|
13
18
|
ansible.builtin.assert:
|
|
14
19
|
that:
|
|
@@ -27,10 +32,12 @@
|
|
|
27
32
|
- ansible_facts.getent_passwd[development_user][1] | int == development_uid | int
|
|
28
33
|
- ansible_facts.getent_passwd[development_user][4] == development_home
|
|
29
34
|
tags: [always]
|
|
30
|
-
-
|
|
35
|
+
- name: Recover or refuse a broken package database
|
|
36
|
+
ansible.builtin.import_tasks: tasks/dpkg.yml
|
|
31
37
|
# Memory and disk safety must land even when the disk is nearly full.
|
|
32
38
|
# fleet-repair re-applies them alone (--tags disk) for worker:capacity:*.
|
|
33
|
-
-
|
|
39
|
+
- name: Apply memory and disk safety
|
|
40
|
+
ansible.builtin.import_tasks: tasks/resources.yml
|
|
34
41
|
tags: [disk]
|
|
35
42
|
- name: Inspect capacity before package and service changes
|
|
36
43
|
ansible.builtin.command:
|
|
@@ -43,11 +50,12 @@
|
|
|
43
50
|
environment:
|
|
44
51
|
FLEET_HOME: '{{ development_home }}'
|
|
45
52
|
changed_when: false
|
|
53
|
+
check_mode: false
|
|
46
54
|
register: capacity
|
|
47
55
|
- name: Preserve management capacity instead of installing into an exhausted disk
|
|
48
56
|
ansible.builtin.assert:
|
|
49
57
|
that:
|
|
50
|
-
- item.freeMiB >=
|
|
58
|
+
- item.freeMiB >= worker_minimum_free_disk_mib | int
|
|
51
59
|
- item.freeInodes >= minimumFreeInodes
|
|
52
60
|
fail_msg: RESOURCE_CAPACITY_REQUIRED; inspect disk space and inodes on the reported filesystem. Preserve developer work; the disk GC removes only build output and caches, so what remains needs a person.
|
|
53
61
|
loop: '{{ capacity.stdout | from_json }}'
|
|
@@ -58,18 +66,36 @@
|
|
|
58
66
|
ansible.builtin.assert:
|
|
59
67
|
that: "'docker-ce' not in ansible_facts.packages and 'containerd.io' not in ansible_facts.packages"
|
|
60
68
|
fail_msg: DOCKER_OWNER_CONFLICT; migrate the existing Docker owner explicitly before applying the Ubuntu-native baseline.
|
|
69
|
+
when: worker_containers_on | bool
|
|
61
70
|
- name: Inspect existing service ownership
|
|
62
71
|
ansible.builtin.service_facts:
|
|
63
72
|
tasks:
|
|
64
|
-
-
|
|
65
|
-
|
|
66
|
-
-
|
|
73
|
+
- name: Install the pinned packages
|
|
74
|
+
ansible.builtin.import_tasks: tasks/packages.yml
|
|
75
|
+
- name: Configure Docker
|
|
76
|
+
ansible.builtin.import_tasks: tasks/docker.yml
|
|
77
|
+
when: worker_containers_on | bool
|
|
78
|
+
- name: Remove what dev-fleet installed for containers
|
|
79
|
+
ansible.builtin.import_tasks: tasks/containers-off.yml
|
|
80
|
+
when: not worker_containers_on | bool
|
|
81
|
+
- name: Install the browser for web workers
|
|
82
|
+
ansible.builtin.import_tasks: tasks/browser.yml
|
|
67
83
|
when: "'web' in worker_tool_profiles | default([])"
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
-
|
|
84
|
+
- name: Keep the always-on services running
|
|
85
|
+
ansible.builtin.import_tasks: tasks/services.yml
|
|
86
|
+
- name: Apply the power policy
|
|
87
|
+
ansible.builtin.import_tasks: tasks/power.yml
|
|
88
|
+
- name: Configure the development user
|
|
89
|
+
ansible.builtin.import_tasks: tasks/user.yml
|
|
90
|
+
- name: Configure SSH
|
|
91
|
+
ansible.builtin.import_tasks: tasks/ssh.yml
|
|
92
|
+
# A container's network belongs to its host, which owns the firewall.
|
|
93
|
+
- name: Admit inbound traffic only over loopback and the tailnet
|
|
94
|
+
ansible.builtin.import_tasks: tasks/firewall.yml
|
|
95
|
+
when: ansible_facts.virtualization_type not in ['docker', 'container', 'podman', 'lxc', 'systemd-nspawn']
|
|
96
|
+
- name: Install the credential helpers
|
|
97
|
+
ansible.builtin.import_tasks: tasks/credentials.yml
|
|
73
98
|
when: development_source is defined
|
|
74
|
-
-
|
|
99
|
+
- name: Sign the worker in to GitHub
|
|
100
|
+
ansible.builtin.import_tasks: tasks/github.yml
|
|
75
101
|
when: development_source is defined
|
|
@@ -32,7 +32,7 @@ the last one says how many MiB were reclaimed.
|
|
|
32
32
|
|
|
33
33
|
Usage: disk-gc.py --home H [--path P ...] [--idle-days N] [--pressure-mib N]
|
|
34
34
|
[--pressure-inode-percent N] [--pressure-idle-hours N] [--docker-age-hours N]
|
|
35
|
-
[--pressure-docker-age-hours N] [--pressure-cache DIR ...]
|
|
35
|
+
[--pressure-docker-age-hours N] [--no-docker] [--pressure-cache DIR ...]
|
|
36
36
|
[--worktree-root DIR ...] [--worktree-idle-days N]
|
|
37
37
|
[--cache-cap DIR=MIB ...] [--keep-newest DIR=N ...]
|
|
38
38
|
[--mise PATH] [--user NAME]
|
|
@@ -476,6 +476,9 @@ def main(argv):
|
|
|
476
476
|
parser.add_argument('--pressure-idle-hours', type=float, default=12)
|
|
477
477
|
parser.add_argument('--docker-age-hours', type=int, default=168)
|
|
478
478
|
parser.add_argument('--pressure-docker-age-hours', type=int, default=24)
|
|
479
|
+
# A worker without containers (fleet.json features.containers) has no
|
|
480
|
+
# Docker to prune.
|
|
481
|
+
parser.add_argument('--no-docker', action='store_true')
|
|
479
482
|
parser.add_argument('--pressure-cache', action='append', default=[])
|
|
480
483
|
parser.add_argument('--worktree-root', action='append', default=[])
|
|
481
484
|
parser.add_argument('--worktree-idle-days', type=float, default=30)
|
|
@@ -523,11 +526,12 @@ def main(argv):
|
|
|
523
526
|
prune = ['runuser', '-u', args.user, '--', 'env', f'HOME={home}'] + prune
|
|
524
527
|
command(prune, args.dry_run, 'mise_prune')
|
|
525
528
|
reclaimed += max(0, before_mise - size_of(installs))
|
|
526
|
-
if not args.home_only:
|
|
529
|
+
if not args.home_only and not args.no_docker:
|
|
527
530
|
command(['docker', 'image', 'prune', '--all', '--force', '--filter',
|
|
528
531
|
f'until={docker_age}h'], args.dry_run, 'docker_images')
|
|
529
532
|
command(['docker', 'builder', 'prune', '--all', '--force', '--filter',
|
|
530
533
|
f'until={docker_age}h'], args.dry_run, 'docker_build_cache')
|
|
534
|
+
if not args.home_only:
|
|
531
535
|
command(['apt-get', 'clean'], args.dry_run, 'apt_cache')
|
|
532
536
|
|
|
533
537
|
after = {p: free_bytes(p) for p in paths}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Managed by personal Fleet; inbound only over loopback and the tailnet (worker exposure policy).
|
|
2
|
+
# Loaded at boot by personal-fleet-firewall.service. Replaces only its own
|
|
3
|
+
# table: Docker's and Tailscale's tables are left to their owners.
|
|
4
|
+
table inet personal_fleet
|
|
5
|
+
delete table inet personal_fleet
|
|
6
|
+
|
|
7
|
+
table inet personal_fleet {
|
|
8
|
+
chain input {
|
|
9
|
+
type filter hook input priority filter - 10; policy drop;
|
|
10
|
+
iif "lo" accept
|
|
11
|
+
ct state invalid drop
|
|
12
|
+
ct state established,related accept
|
|
13
|
+
iifname "{{ exposure.tailnetInterface }}" accept
|
|
14
|
+
{% if firewall_docker | bool %}
|
|
15
|
+
{% for interface in exposure.containerInterfaces %}
|
|
16
|
+
iifname "{{ interface }}" accept comment "containers reach the host"
|
|
17
|
+
{% endfor %}
|
|
18
|
+
{% endif %}
|
|
19
|
+
udp dport {{ exposure.tailscalePort }} accept comment "tailscaled"
|
|
20
|
+
meta l4proto ipv6-icmp accept
|
|
21
|
+
meta l4proto icmp accept
|
|
22
|
+
udp sport 67 udp dport 68 accept comment "dhcp"
|
|
23
|
+
ip6 saddr fe80::/10 udp sport 547 udp dport 546 accept comment "dhcpv6"
|
|
24
|
+
{% if firewall_public_ssh | bool %}
|
|
25
|
+
{% for port in firewall_ssh_ports %}
|
|
26
|
+
tcp dport {{ port }} accept comment "public-ssh"
|
|
27
|
+
{% endfor %}
|
|
28
|
+
{% endif %}
|
|
29
|
+
{% for entry in firewall_policy.publicPorts | default([]) %}
|
|
30
|
+
{{ entry.protocol }} dport {{ entry.port }} accept comment "public-port"
|
|
31
|
+
{% endfor %}
|
|
32
|
+
}
|
|
33
|
+
{% if firewall_docker | bool %}
|
|
34
|
+
|
|
35
|
+
# A published container port is reached by forwarding, not input: from
|
|
36
|
+
# outside the tailnet it is dropped before Docker's own chains accept it.
|
|
37
|
+
chain forward {
|
|
38
|
+
type filter hook forward priority filter - 10; policy accept;
|
|
39
|
+
ct state established,related accept
|
|
40
|
+
iifname "{{ exposure.tailnetInterface }}" accept
|
|
41
|
+
{% for interface in exposure.containerInterfaces %}
|
|
42
|
+
iifname "{{ interface }}" accept
|
|
43
|
+
{% endfor %}
|
|
44
|
+
ct status dnat drop comment "container-port"
|
|
45
|
+
{% for interface in exposure.containerInterfaces %}
|
|
46
|
+
oifname "{{ interface }}" drop comment "container-port"
|
|
47
|
+
{% endfor %}
|
|
48
|
+
}
|
|
49
|
+
{% endif %}
|
|
50
|
+
}
|