diff --git a/.gitignore b/.gitignore index 5d755fe3..52a5bcf3 100644 --- a/.gitignore +++ b/.gitignore @@ -129,8 +129,6 @@ dmypy.json .k8s/ test-results/ k8s/overlays/local/config.local.env -# The admin SSH key `make deploy` generates -k8s/base/ssh/ # Throwaway venv built by "make smoke" .smoke/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fe3dbe39..cb75c017 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,6 +30,8 @@ repos: - id: detect-aws-credentials args: [--allow-missing-credentials] - id: detect-private-key + # The local Phorge's dev key, public on purpose, see k8s/base/ssh/README.md + exclude: ^k8s/base/ssh/phabfive-insecure$ - id: end-of-file-fixer exclude: | (?x) diff --git a/CHANGELOG.md b/CHANGELOG.md index 790059dd..0f5051bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -180,6 +180,16 @@ ## Other Notes +* The local Phorge's admin SSH key is a known key, committed as + `k8s/base/ssh/phabfive-insecure` the way the API token is, instead of one `make deploy` + generated into whichever worktree deployed. Every worktree can now use git over SSH, + and run `tests/k8s/test_ssh.py`, against the one shared instance, and `make reset` + keeps the key. ssh refuses a private key others can read, so `make deploy` and + `make ssh-forward` copy it to `.k8s/phabfive-insecure` with mode 0600. A running + instance picks the new key up on its next start, next to the old one. `make creds` + prints the whole `GIT_SSH_COMMAND`, with `StrictHostKeyChecking=accept-new`, since every + `make reset` brings a new host key + * The local Phorge seeds a hidden column, "Won't Do", on the Development board, and creates the column of each milestone on its parent's board, which Phorge otherwise creates only when the board is first opened in a browser. #588 diff --git a/Makefile b/Makefile index a9b56637..c37eaa3d 100644 --- a/Makefile +++ b/Makefile @@ -31,11 +31,15 @@ VERSION ?= $(shell cat $(BUILD_DIR)/version 2>/dev/null || echo stable) # PHP series to build on (8.0, 8.3, 8.4, 8.5), empty for the newest VERSION runs on PHP ?= -# The admin's SSH key, generated on first deploy and registered by the sshkeys -# seed module. SSH_PORT is PHORGE_SSH_PORT, where Phorge says the clone URIs -# are, so it is where `make ssh-forward` listens: config.local.env overrides +# The admin's SSH key, registered by the sshkeys seed module. A known key +# committed in k8s/base/ssh/, like the API token: public on purpose, so every +# worktree reaches the shared instance with it. ssh refuses a private key +# others can read and git checks it out 0644, so ssh is given a 0600 copy. +# SSH_PORT is PHORGE_SSH_PORT, where Phorge says the clone URIs are, so it is +# where `make ssh-forward` listens: config.local.env overrides # k8s/base/config.env, as it does in the deployment. -SSH_KEY := k8s/base/ssh/admin +SSH_KEY_SRC := k8s/base/ssh/phabfive-insecure +SSH_KEY := $(BUILD_DIR)/phabfive-insecure SSH_PORT := $(or $(lastword $(shell sed -n 's/^PHORGE_SSH_PORT=//p' k8s/base/config.env k8s/overlays/local/config.local.env 2>/dev/null)),2222) GIT_SSH_COMMAND = ssh -i $(CURDIR)/$(SSH_KEY) -o UserKnownHostsFile=$(CURDIR)/$(BUILD_DIR)/known_hosts -o StrictHostKeyChecking=accept-new @@ -175,9 +179,9 @@ phorge-image: check-tools ## build the phorge image of VERSION (stable, master, $(K3D) image import -c $(CLUSTER) $(PHORGE_IMAGE):$$tag; \ echo $$tag > $(BUILD_DIR)/image-tag -$(SSH_KEY): +$(SSH_KEY): $(SSH_KEY_SRC) @mkdir -p $(@D) - ssh-keygen -q -t ed25519 -N '' -C phabfive-dev -f $@ + install -m 600 $< $@ deploy: check-tools $(SSH_KEY) ## apply the manifests of OVERLAY (local or ci) with the imported image @test -f $(BUILD_DIR)/image-tag || { echo "Error: no image imported yet, run make phorge-image"; exit 1; } @@ -201,8 +205,8 @@ down: check-tools ## stop phorge and mariadb, keep data reset: check-tools ## DELETE the phorge namespace with all its data and clear its completion cache mise exec -- kubectl --context $(KUBE_CONTEXT) delete namespace $(NAMESPACE) --ignore-not-found --wait - @# The host key went with the data, and the next instance gets a new admin key - rm -f $(SSH_KEY) $(SSH_KEY).pub $(BUILD_DIR)/known_hosts + @# The host key went with the data, the admin key is the same on every instance + rm -f $(BUILD_DIR)/known_hosts @$(MAKE) --no-print-directory clear-cache logs: check-tools ## follow phorge logs @@ -218,7 +222,7 @@ shell: check-tools ## open shell in the phorge pod creds: check-tools ## print credentials of the running phorge @$(KUBECTL) exec deploy/phorge -- /usr/local/bin/lib/banner.sh -ssh-forward: check-tools ## forward localhost:PHORGE_SSH_PORT (2222) to phorge's sshd, for git over SSH (runs until interrupted) +ssh-forward: check-tools $(SSH_KEY) ## forward localhost:PHORGE_SSH_PORT (2222) to phorge's sshd, for git over SSH (runs until interrupted) @echo "git over SSH, while this runs:" @echo " GIT_SSH_COMMAND='$(GIT_SSH_COMMAND)' \\" @echo " git clone ssh://git@phorge.localhost:$(SSH_PORT)/source/gunnar-firmware.git" @@ -227,7 +231,7 @@ ssh-forward: check-tools ## forward localhost:PHORGE_SSH_PORT (2222) to phorge's ##@ Test -validate: $(SSH_KEY) ## validate the rendered manifests of all overlays +validate: ## validate the rendered manifests of all overlays @touch k8s/overlays/local/config.local.env @for overlay in k8s/overlays/*/; do \ mise exec -- kubectl kustomize $$overlay | mise exec -- kubeconform -strict -summary || exit 1; \ diff --git a/docs/phorge-setup.md b/docs/phorge-setup.md index 977c8d86..27024dda 100644 --- a/docs/phorge-setup.md +++ b/docs/phorge-setup.md @@ -211,8 +211,12 @@ client can read it: `passphrase show K1 -s` fails the way K2 does. Phorge serves git over SSH as well as HTTP, from a second container in the same pod: `web` runs Apache and the daemons, `sshd` runs OpenSSH with Phorge's `ssh-auth` as its `AuthorizedKeysCommand`. The admin account has an SSH key, -which `make deploy` generates into `k8s/base/ssh/admin` (gitignored) and the -`sshkeys` seed module registers. +which the `sshkeys` seed module registers: `k8s/base/ssh/phabfive-insecure`, a +known key committed to the repository like the API token, so every worktree and +every instance use the same one. **It is public, never authorize it anywhere +else.** ssh refuses a private key others can read, and git checks files out +readable, so `make deploy` and `make ssh-forward` copy it to +`.k8s/phabfive-insecure` with mode 0600, and that copy is the one to use. No port on the host reaches sshd. The cluster's published ports are shared by every Dynamist dev app (`k8s/cluster/k3d.yaml`), so instead `make ssh-forward` @@ -221,7 +225,7 @@ and prints the command to use: ```bash make ssh-forward # in one terminal -GIT_SSH_COMMAND="ssh -i $PWD/k8s/base/ssh/admin -o UserKnownHostsFile=$PWD/.k8s/known_hosts -o StrictHostKeyChecking=accept-new" \ +GIT_SSH_COMMAND="ssh -i $PWD/.k8s/phabfive-insecure -o UserKnownHostsFile=$PWD/.k8s/known_hosts -o StrictHostKeyChecking=accept-new" \ git clone ssh://git@phorge.localhost:2222/source/gunnar-firmware.git ``` @@ -238,8 +242,9 @@ and forces each to run `ssh-exec` as its owner, and `ssh-exec` runs git through A push runs Phorge's commit hook, so the new commits are known at once. The sshd host key is kept on the volume, so `known_hosts` stays valid across -`make down`/`make up`. `make reset` deletes it, the admin key and -`.k8s/known_hosts` together. +`make down`/`make up`. `make reset` deletes it and `.k8s/known_hosts` +together, and `StrictHostKeyChecking=accept-new` records the next instance's +host key on first contact. The admin key stays the same. Seeded repositories have dangerous change protection on, so a pushed branch cannot be deleted again over SSH. Push to a repository of your own for @@ -401,7 +406,7 @@ phorge/seed/ | `tasks` | users, projects, spaces | tasks, through `maniphest.edit` | | `credentials` | auth | Passphrase credentials, through Phorge's editors | | `repositories` | auth, users | hosted git repositories and their history, through `diffusion.repository.edit` | -| `sshkeys` | auth | the admin's SSH key from `k8s/base/ssh/admin.pub`, through Phorge's editor | +| `sshkeys` | auth | the admin's SSH key from `k8s/base/ssh/phabfive-insecure.pub`, through Phorge's editor | Every module is idempotent - it creates only what is missing - so restarting the pod duplicates nothing. `PHORGE_SEED` is `all` by default; to seed only part of diff --git a/k8s/base/kustomization.yaml b/k8s/base/kustomization.yaml index f9d64679..b3336de3 100644 --- a/k8s/base/kustomization.yaml +++ b/k8s/base/kustomization.yaml @@ -17,6 +17,6 @@ configMapGenerator: secretGenerator: - name: phorge-secret envs: [secret.env] - # The admin's SSH public key, generated by `make deploy` (gitignored) + # The admin's SSH public key, a known dev key committed with its private half - name: phorge-ssh-key - files: [admin.pub=ssh/admin.pub] + files: [admin.pub=ssh/phabfive-insecure.pub] diff --git a/k8s/base/phorge.yaml b/k8s/base/phorge.yaml index 9896e818..039747fa 100644 --- a/k8s/base/phorge.yaml +++ b/k8s/base/phorge.yaml @@ -159,7 +159,7 @@ spec: - name: ssh-key secret: secretName: phorge-ssh-key - # Only the public half, the private one stays in k8s/base/ssh/ + # Only the public half of k8s/base/ssh/phabfive-insecure optional: true - name: data persistentVolumeClaim: diff --git a/k8s/base/ssh/README.md b/k8s/base/ssh/README.md new file mode 100644 index 00000000..557a53cc --- /dev/null +++ b/k8s/base/ssh/README.md @@ -0,0 +1,12 @@ +# The local Phorge's admin SSH key + +`phabfive-insecure` is a **publicly known** private key, committed on purpose like the +API token `api-supersecr3tapikeyfordevelop1`. The `sshkeys` seed module registers its +public half for the admin of the disposable local Phorge, so git over SSH works from +every worktree against the one shared instance. + +Never authorize it anywhere else, and never load it into an agent. + +ssh refuses a private key that others can read, and git checks this one out 0644, so +use the 0600 copy `make deploy` and `make ssh-forward` write to `.k8s/phabfive-insecure`. +See docs/phorge-setup.md, "Git over SSH". diff --git a/k8s/base/ssh/phabfive-insecure b/k8s/base/ssh/phabfive-insecure new file mode 100644 index 00000000..be5dc594 --- /dev/null +++ b/k8s/base/ssh/phabfive-insecure @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACAskuIZ+ML6PhhanBgj/1IgAXHDRQwXlvZfO6QcX6EZZwAAAJgRQXfZEUF3 +2QAAAAtzc2gtZWQyNTUxOQAAACAskuIZ+ML6PhhanBgj/1IgAXHDRQwXlvZfO6QcX6EZZw +AAAEAUsSAfh+3SufWx+o56JSRbn6qiW4aVOtGi3aXuTuuseiyS4hn4wvo+GFqcGCP/UiAB +ccNFDBeW9l87pBxfoRlnAAAAEXBoYWJmaXZlLWluc2VjdXJlAQIDBA== +-----END OPENSSH PRIVATE KEY----- diff --git a/k8s/base/ssh/phabfive-insecure.pub b/k8s/base/ssh/phabfive-insecure.pub new file mode 100644 index 00000000..5b5e22b1 --- /dev/null +++ b/k8s/base/ssh/phabfive-insecure.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICyS4hn4wvo+GFqcGCP/UiABccNFDBeW9l87pBxfoRln phabfive-insecure diff --git a/phorge/lib/banner.sh b/phorge/lib/banner.sh index 71f7ff22..fad31628 100755 --- a/phorge/lib/banner.sh +++ b/phorge/lib/banner.sh @@ -176,8 +176,9 @@ print_banner() { echo " $PHORGE_URL" echo "" if [ -n "${PHORGE_SSH_USER:-}" ]; then - echo "🔗 git over SSH, after 'make ssh-forward' (key: k8s/base/ssh/admin):" - echo " ssh://${PHORGE_SSH_USER}@${PHORGE_SSH_HOST:-phorge.localhost}:${PHORGE_SSH_PORT:-22}/source/.git" + echo "🔗 git over SSH, after 'make ssh-forward', from the phabfive checkout:" + echo " GIT_SSH_COMMAND='ssh -i .k8s/phabfive-insecure -o UserKnownHostsFile=.k8s/known_hosts -o StrictHostKeyChecking=accept-new' \\" + echo " git clone ssh://${PHORGE_SSH_USER}@${PHORGE_SSH_HOST:-phorge.localhost}:${PHORGE_SSH_PORT:-22}/source/.git" echo "" fi echo "💡 TIP: The API token works immediately without logging in!" diff --git a/phorge/seed/modules/sshkeys.php b/phorge/seed/modules/sshkeys.php index 83e0f887..95f36dd6 100644 --- a/phorge/seed/modules/sshkeys.php +++ b/phorge/seed/modules/sshkeys.php @@ -4,7 +4,7 @@ * The admin's SSH public key, so git over SSH works against the seeded * repositories. * - * `make deploy` generates the keypair in k8s/base/ssh/ (gitignored), and the + * A known dev keypair, committed in k8s/base/ssh/phabfive-insecure, and the * public half reaches the pod as the Secret phorge-ssh-key. There is no * Conduit method for SSH keys, so like credentials this goes through Phorge's * editor, with the transactions PhabricatorAuthSSHKeyEditController applies. @@ -15,7 +15,7 @@ final class PhabfiveSSHKeysSeedModule extends PhabfiveSeedModule { const KEY_PATH = '/etc/phorge-seed/ssh/admin.pub'; - const KEY_NAME = 'phabfive-dev'; + const KEY_NAME = 'phabfive-insecure'; public function getKey() { return 'sshkeys'; diff --git a/tests/k8s/test_ssh.py b/tests/k8s/test_ssh.py index 490d24fa..540d99ce 100644 --- a/tests/k8s/test_ssh.py +++ b/tests/k8s/test_ssh.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- """git over SSH, through the sshd container and `kubectl port-forward`. -Reached the way `make ssh-forward` reaches it, with the admin key `make deploy` -generated and the sshkeys seed module registered. +Reached the way `make ssh-forward` reaches it, with the known dev key +k8s/base/ssh/phabfive-insecure that the sshkeys seed module registered. """ # python std lib import os +import shutil import socket import subprocess import time @@ -17,7 +18,7 @@ from tests.k8s.conftest import KUBE_CONTEXT, NAMESPACE, ROOT -ADMIN_KEY = ROOT / "k8s/base/ssh/admin" +ADMIN_KEY = ROOT / "k8s/base/ssh/phabfive-insecure" SSH_HOST = "phorge.localhost" SSH_PORT = 2222 @@ -61,8 +62,12 @@ def ssh_port(): def git(ssh_port, tmp_path): """Run git with ssh pointed at the forwarded port, as `key` (the admin's by default).""" known_hosts = tmp_path / "known_hosts" + # ssh refuses a private key others can read, and git checks it out 0644 + admin_key = tmp_path / "phabfive-insecure" + shutil.copyfile(ADMIN_KEY, admin_key) + admin_key.chmod(0o600) - def run(*args, key=ADMIN_KEY, check=True, cwd=None): + def run(*args, key=admin_key, check=True, cwd=None): command = ( f"ssh -i {key} -o IdentitiesOnly=yes -o BatchMode=yes" f" -o UserKnownHostsFile={known_hosts} -o StrictHostKeyChecking=accept-new"