Skip to content

ci: deploy over SSH with a forced command instead of Azure OIDC - #4

Merged
mattDev0 merged 1 commit into
mainfrom
feat/ssh-deploy
Aug 31, 2026
Merged

ci: deploy over SSH with a forced command instead of Azure OIDC#4
mattDev0 merged 1 commit into
mainfrom
feat/ssh-deploy

Conversation

@mattDev0

Copy link
Copy Markdown
Owner

Restores automated deployment after the subscription change. Rationale is recorded in docs/adr/adr_02_ssh_deploy.md.

Why the OIDC deploy can't come back

On the replacement "Azure for Students" subscription:

az ad app create --display-name github-dcc-deploy
ERROR: Insufficient privileges to complete the operation.

The account is Owner on the subscription, but app registration is an Entra directory permission and the university tenant blocks it — Azure RBAC can't grant it. No app registration means no service principal, so neither OIDC federation nor a client secret is available.

What replaces it

A dedicated deploy key restricted to a forced command on the host:

command="/usr/local/bin/dcc-deploy",no-agent-forwarding,no-port-forwarding,
no-pty,no-user-rc,no-X11-forwarding ssh-ed25519 AAAA... github-actions-deploy

The wrapper is root-owned, lives outside the git checkout, and accepts only a bare commit SHA:

SHA="${SSH_ORIGINAL_COMMAND:-}"
[[ "$SHA" =~ ^[0-9a-f]{40}$ ]] || { echo "refusing..."; exit 2; }

Verified on the host:

Attempt Result
"whoami" refused
"rm -rf /" refused
interactive session refused, no PTY
a real 40-char SHA deploys that commit

The host key is pinned via DEPLOY_KNOWN_HOSTS with StrictHostKeyChecking=yes, so there's no trust-on-first-use window.

Three bugs fixed in deploy.sh along the way

COMMIT_SHA was being clobbered. .env on the host carries a COMMIT_SHA from the previous deploy, and set -a; . .env overwrote the incoming value — every deploy would have silently redeployed the old commit. The incoming value is now captured first and takes precedence.

COMMIT_SHA is now required. It previously fell back to a latest tag that this pipeline has never published.

GITHUB_TOKEN is now optional, and the tokenised remote URL is no longer left behind in .git/config.

Secrets

DEPLOY_HOST, DEPLOY_USER, DEPLOY_SSH_KEY, DEPLOY_KNOWN_HOSTS are set. The six AZURE_* secrets are now dead — they point at a disabled subscription and a destroyed host — and should be deleted. AZURE_SSH_KEY is a private key with no remaining purpose.

Merge order matters

Merge #3 (docker GID) first. The first SSH deploy runs git reset --hard origin/main, which will overwrite the host-local GID patch. If this merges first, the Docker panel breaks until #3 lands.

Security note

This is a genuine downgrade from OIDC: a long-lived private key now sits in GitHub secrets where previously nothing durable was stored. That's the cost of the tenant restriction, not a preference. The forced command limits a compromise to "can trigger a deploy of an already-built commit" rather than shell access. If app registration ever becomes available, the previous implementation is in git history.

The previous subscription was disabled, taking the production VM with it.
On the replacement "Azure for Students" subscription, Entra app registration
is blocked by the university tenant:

    az ad app create -> Insufficient privileges to complete the operation

The account is Owner on the subscription, but app registration is a directory
permission that Azure RBAC cannot grant, so there is no service principal and
therefore no OIDC federation and no client secret. The keyless deploy cannot
be recreated on this account.

Replaces it with a dedicated deploy key restricted to a forced command on the
host, so the key can trigger a deploy of one commit and nothing else - no
shell, no arbitrary commands, no port forwarding. The host key is pinned via
a secret rather than trusted on first use.

Also in deploy.sh:
- Capture COMMIT_SHA before sourcing .env. .env carries a COMMIT_SHA from the
  previous deploy, and "set -a; . .env" would overwrite the incoming value,
  silently redeploying the old commit on every run.
- Require COMMIT_SHA rather than defaulting to a "latest" tag that this
  pipeline never publishes.
- Make GITHUB_TOKEN optional. The repository and its GHCR images are public,
  and the forced command deliberately passes no token.
- Stop leaving a tokenised remote URL in .git/config.

Rationale and the security trade-off are recorded in
docs/adr/adr_02_ssh_deploy.md.
@mattDev0
mattDev0 merged commit 3a303f0 into main Aug 31, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant