Merge nucleic/vivid-glass-urchin-xoym into main
This commit is contained in:
+80
-19
@@ -162,8 +162,9 @@ SSH server, so the build appears to hang. See
|
||||
|
||||
Virtualization.framework refuses to run unless the calling binary carries the
|
||||
`com.apple.security.virtualization` entitlement, and entitlements are only honoured on a signed
|
||||
binary inside a proper `.app` bundle. Ad-hoc signing (`codesign -s -`) satisfies this — **no paid
|
||||
Apple developer account is needed.**
|
||||
binary inside a proper `.app` bundle. That entitlement is not restricted, so ad-hoc signing
|
||||
(`codesign -s -`) satisfies it — **the runner works with no Apple developer account.** A Developer
|
||||
ID certificate buys something different and worth having; see [Code signing](#code-signing) below.
|
||||
|
||||
```sh
|
||||
git clone <this repo> && cd gitea-macos-runner
|
||||
@@ -176,7 +177,7 @@ make install
|
||||
| --- | --- |
|
||||
| `make build` | `swift build -c release --arch arm64` |
|
||||
| `make bundle` | Assemble `GiteaMacosRunner.app` around the binary: `Contents/MacOS/gitea-macos-runner`, `Contents/Info.plist`, and `Contents/Resources/` (`provision.sh`, `launchd.plist.template`, `config.example.json`) |
|
||||
| `make sign` | `codesign --sign - --entitlements …` (ad-hoc) and print the resulting entitlements |
|
||||
| `make sign` | Sign the bundle with the virtualization entitlement — Developer ID when a matching certificate is in the keychain, ad-hoc otherwise — then print the entitlements and the resulting identity |
|
||||
| `make all` | `build` + `bundle` + `sign`. The default target. |
|
||||
| `make install` | Runs `all`, copies the app to `~/Applications`, and symlinks the CLI to `/usr/local/bin/gitea-macos-runner` |
|
||||
| `make dev` | Debug build + bundle + sign, for fast iteration. Does **not** install. |
|
||||
@@ -192,6 +193,58 @@ code looks in `Contents/Resources` first and only then falls back to
|
||||
repo-relative paths, so an installed `.app` missing them is a working binary
|
||||
with three broken commands.
|
||||
|
||||
### Code signing
|
||||
|
||||
`make sign` picks its identity automatically:
|
||||
|
||||
| Keychain state | What you get |
|
||||
| --- | --- |
|
||||
| A `Developer ID Application` certificate whose team matches `TEAM_ID` | Developer ID signature, hardened runtime (`--options runtime`), trusted timestamp (`--timestamp`) |
|
||||
| No matching certificate | Ad-hoc signature (`codesign --sign -`), with a warning |
|
||||
|
||||
Both produce a bundle that boots VMs — the virtualization entitlement is not restricted, and needs
|
||||
no provisioning profile on either path. What differs is **code identity stability**, and that is the
|
||||
whole reason to prefer Developer ID:
|
||||
|
||||
- A Developer ID signature carries a designated requirement anchored to your team
|
||||
(`… and certificate leaf[subject.OU] = L7UDTQ6F5W`). Every subsequent build satisfies it, so macOS
|
||||
recognises rebuild after rebuild as *the same program*.
|
||||
- An ad-hoc signature has no anchor, so the system falls back to the main executable's Mach-O UUID —
|
||||
which the linker regenerates on essentially every link. Each `make install` presents a program
|
||||
macOS has never seen before.
|
||||
|
||||
The practical consequence is Local Network privacy (§2.6): per
|
||||
[TN3179](https://developer.apple.com/documentation/technotes/tn3179-understanding-local-network-privacy)
|
||||
the grant "uses your main executable UUID as part of its implementation", so under ad-hoc signing it
|
||||
is silently withdrawn by the next rebuild. Under Developer ID it survives.
|
||||
|
||||
The team is baked into the `Makefile` as a default; override it for your own certificate:
|
||||
|
||||
```sh
|
||||
make install TEAM_ID=ABCDE12345 # your Developer ID team
|
||||
make install TEAM_ID= # force ad-hoc even if a certificate exists
|
||||
```
|
||||
|
||||
Confirm what actually landed — `doctor`'s `code identity` check reports it, or ask `codesign`:
|
||||
|
||||
```sh
|
||||
codesign -dvv ~/Applications/GiteaMacosRunner.app
|
||||
# Identifier=xyz.blakeslee.gitea-macos-vm-orchestrator
|
||||
# CodeDirectory v=20500 … flags=0x10000(runtime)
|
||||
# Authority=Developer ID Application: Your Name (ABCDE12345)
|
||||
# TeamIdentifier=ABCDE12345
|
||||
```
|
||||
|
||||
`TeamIdentifier=not set` and `flags=0x2(adhoc)` mean the ad-hoc path was taken.
|
||||
|
||||
Two things this deliberately does **not** do. The bundle is not **notarized**: notarization matters
|
||||
for software distributed to other Macs, where Gatekeeper checks the quarantine bit, and this app is
|
||||
built and installed in place. `spctl -a` therefore reports `rejected: Unnotarized Developer ID`,
|
||||
which is expected and does not stop anything here. Signing does require network access for
|
||||
`--timestamp`, so an offline host falls back to ad-hoc. And the entitlements list stays minimal:
|
||||
`com.apple.vm.networking` — needed only for bridged networking, and genuinely restricted — is not
|
||||
requested. See [DESIGN.md](DESIGN.md).
|
||||
|
||||
If `/usr/local/bin` isn't writable, `make install` says so and prints the `sudo ln -sf …` command to
|
||||
run yourself.
|
||||
|
||||
@@ -420,25 +473,31 @@ a **warning** when an allowlist exists but does not. Both keys are documented by
|
||||
[TN3179](https://developer.apple.com/documentation/technotes/tn3179-understanding-local-network-privacy)
|
||||
and are the workaround [Tart's FAQ](https://tart.run/faq/) recommends for the same problem.
|
||||
|
||||
**Approving interactively instead.** The app cannot be pre-approved: it appears under **System
|
||||
Settings → Privacy & Security → Local Network** only *after* it has actually attempted a connection
|
||||
to a guest. An empty list is expected on a fresh install and does not mean anything is broken. To
|
||||
create the entry and answer the prompt, run one boot by hand from a Terminal in the GUI session:
|
||||
**Why not just approve the prompt?** Because on this host there is usually nothing able to show it,
|
||||
and when something does, it is attributed to the wrong program.
|
||||
|
||||
```sh
|
||||
gitea-macos-runner vm boot --image default
|
||||
```
|
||||
An app appears under **System Settings → Privacy & Security → Local Network** only *after* it has
|
||||
attempted a connection to a guest, so an empty list on a fresh install is expected and means
|
||||
nothing is broken. It cannot be pre-approved. But the two obvious ways to trigger the prompt both
|
||||
miss:
|
||||
|
||||
and click **Allow**. Do not wait for the LaunchAgent to hit it — a background agent has no way to
|
||||
answer the prompt.
|
||||
- **From the LaunchAgent.** A background agent has no UI, so the prompt has nowhere to appear. The
|
||||
connection is simply denied, and it surfaces as `No route to host` (errno 65) — not as a
|
||||
permission error.
|
||||
- **By hand from a Terminal**, e.g. `gitea-macos-runner vm boot --image default`. macOS assigns the
|
||||
privacy decision to the *responsible process*, and a binary exec'd from a shell is Terminal's
|
||||
responsibility, not its own. So both the prompt and the Settings row belong to **Terminal**, and
|
||||
approving it there does not carry over to the LaunchAgent. (If you are hunting for a row that
|
||||
seems missing, look for Terminal rather than for "Gitea macOS Runner".)
|
||||
|
||||
> **Caveat with ad-hoc signing.** An interactive grant is not durable for this project's ad-hoc
|
||||
> signed bundle. Local Network privacy does not use TCC; per TN3179 it "uses your main executable
|
||||
> UUID as part of its implementation", and the linker mints a fresh `LC_UUID` on essentially every
|
||||
> rebuild. So `make install` after a code change is liable to present as a new app that must be
|
||||
> approved again — and macOS offers no way to reset a Local Network decision back to undetermined,
|
||||
> so the stale entries accumulate. This is why the allowlist above, which is keyed on the subnet
|
||||
> rather than on the app, is the recommendation for an unattended machine.
|
||||
> **And under ad-hoc signing it is not durable anyway.** Local Network privacy does not use TCC; per
|
||||
> TN3179 it "uses your main executable UUID as part of its implementation", and the linker mints a
|
||||
> fresh `LC_UUID` on essentially every rebuild. So `make install` after a code change presents as a
|
||||
> new app that must be approved again — and macOS offers no way to reset a Local Network decision
|
||||
> back to undetermined, so stale entries accumulate. Signing with a Developer ID certificate fixes
|
||||
> the churn (see [Code signing](#code-signing)); the subnet allowlist above, keyed on the network
|
||||
> rather than on the app, sidesteps the whole mechanism and is the recommendation for an unattended
|
||||
> machine either way.
|
||||
|
||||
---
|
||||
|
||||
@@ -463,6 +522,7 @@ The checks, in order:
|
||||
| `host capability` | Apple Silicon, and host macOS ≥ 26 |
|
||||
| `Virtualization.framework` | `VZVirtualMachine.isSupported` |
|
||||
| `virtualization entitlement` | `com.apple.security.virtualization` on the *running* executable — this is the check that catches running from `.build/` instead of the signed `.app` |
|
||||
| `code identity` | The bundle's signature. Passes naming the identifier, team, and hardened runtime; warns on an ad-hoc signature, because that is what makes Local Network grants evaporate on every rebuild ([Code signing](#code-signing)) |
|
||||
| `configuration` | The config file loads, parses, and passes validation |
|
||||
| `free disk space` | Free space on the `storeDir` volume against `storage.minFreeDiskGB` |
|
||||
| `login.keychain unlocked` | `security show-keychain-info login.keychain` |
|
||||
@@ -470,6 +530,7 @@ The checks, in order:
|
||||
| `registration token` | A static token resolves, or one can be fetched when `fetchRegistrationTokenViaAPI` is on |
|
||||
| `runner download url` | The `gitea-runner` release asset is reachable |
|
||||
| `token file permissions` | Warns — not fails — when a token file is group- or world-readable |
|
||||
| `guest ssh` | Reachability of the most recent guest lease, when there is one. Warns on a timeout, which is most often a stale 24-hour lease for a guest that is already gone |
|
||||
| `local network access` | Passes when a subnet allowlist covers `192.168.64.0/18`; warns when an allowlist exists but is scoped too narrowly; otherwise an informational note about the macOS 15+ Local Network prompt (§2.6) |
|
||||
|
||||
If the config file is missing or invalid, the host checks still run and the rest
|
||||
|
||||
Reference in New Issue
Block a user