diff --git a/.github/workflows/deploy-to-environment.yml b/.github/workflows/deploy-to-environment.yml index fd47e0222..f1e7700e8 100644 --- a/.github/workflows/deploy-to-environment.yml +++ b/.github/workflows/deploy-to-environment.yml @@ -17,6 +17,7 @@ env: jobs: deploy-web-container: runs-on: ubuntu-latest + timeout-minutes: 30 environment: name: ${{ github.event.inputs.environment }} @@ -24,83 +25,79 @@ jobs: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Configure SSH + env: + SSH_HOST: ${{ secrets.APPSERVER_SSH_HOST }} + SSH_PORT: ${{ secrets.APPSERVER_SSH_PORT }} + SSH_USER: ${{ secrets.APPSERVER_SSH_USER }} + SSH_PRIVATE_KEY: ${{ secrets.APPSERVER_SSH_PRIVATE_KEY }} + SSH_KNOWN_HOSTS: ${{ secrets.APPSERVER_SSH_KNOWN_HOSTS }} + run: | + if [ -z "$SSH_PRIVATE_KEY" ]; then + echo "The APPSERVER_SSH_PRIVATE_KEY secret is empty." >&2 + exit 1 + fi + + if [ -z "$SSH_KNOWN_HOSTS" ]; then + echo "The APPSERVER_SSH_KNOWN_HOSTS secret is empty." >&2 + echo "Populate it with the output of: ssh-keyscan -p " >&2 + exit 1 + fi + + mkdir -p ~/.ssh + chmod 700 ~/.ssh + + printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/id_deploy + chmod 600 ~/.ssh/id_deploy + + printf '%s\n' "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts + chmod 600 ~/.ssh/known_hosts + + { + echo "Host appserver" + echo " HostName $SSH_HOST" + echo " Port $SSH_PORT" + echo " User $SSH_USER" + echo " IdentityFile ~/.ssh/id_deploy" + echo " IdentitiesOnly yes" + echo " StrictHostKeyChecking yes" + echo " BatchMode yes" + } > ~/.ssh/config + chmod 600 ~/.ssh/config + - name: Stop service - uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5 - with: - host: ${{ secrets.APPSERVER_SSH_HOST }} - port: ${{ secrets.APPSERVER_SSH_PORT }} - username: ${{ secrets.APPSERVER_SSH_USER }} - key: ${{ secrets.APPSERVER_SSH_PRIVATE_KEY }} - script: | + run: | + ssh appserver ' if [ "$(systemctl --user is-active crypter.service)" = "active" ]; then echo "Stopping service" - systemctl --user stop crypter.service; + systemctl --user stop crypter.service fi + ' - name: Push latest systemctl service file - uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0 - with: - host: ${{ secrets.APPSERVER_SSH_HOST }} - port: ${{ secrets.APPSERVER_SSH_PORT }} - username: ${{ secrets.APPSERVER_SSH_USER }} - key: ${{ secrets.APPSERVER_SSH_PRIVATE_KEY }} - source: Environments/${{ github.event.inputs.environment }}/crypter.service - target: .config/systemd/user/ - strip_components: 2 - + env: + environment_name: ${{ github.event.inputs.environment }} + run: | + ssh appserver 'mkdir -p .config/systemd/user' + scp "Environments/$environment_name/crypter.service" appserver:.config/systemd/user/ + - name: Reload systemctl daemon - uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5 - with: - host: ${{ secrets.APPSERVER_SSH_HOST }} - port: ${{ secrets.APPSERVER_SSH_PORT }} - username: ${{ secrets.APPSERVER_SSH_USER }} - key: ${{ secrets.APPSERVER_SSH_PRIVATE_KEY }} - script: systemctl --user daemon-reload + run: ssh appserver 'systemctl --user daemon-reload' - name: Push latest Docker Compose file - uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0 - with: - host: ${{ secrets.APPSERVER_SSH_HOST }} - port: ${{ secrets.APPSERVER_SSH_PORT }} - username: ${{ secrets.APPSERVER_SSH_USER }} - key: ${{ secrets.APPSERVER_SSH_PRIVATE_KEY }} - source: docker-compose.yml - target: crypter-web-container/ + run: | + ssh appserver 'mkdir -p crypter-web-container' + scp docker-compose.yml appserver:crypter-web-container/ - name: Push latest Docker Compose override file if deploying to Staging server if: github.event.inputs.environment == 'staging' - uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0 - with: - host: ${{ secrets.APPSERVER_SSH_HOST }} - port: ${{ secrets.APPSERVER_SSH_PORT }} - username: ${{ secrets.APPSERVER_SSH_USER }} - key: ${{ secrets.APPSERVER_SSH_PRIVATE_KEY }} - source: docker-compose.override.yml - target: crypter-web-container/ + run: scp docker-compose.override.yml appserver:crypter-web-container/ - name: Pull latest images - uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5 - with: - host: ${{ secrets.APPSERVER_SSH_HOST }} - port: ${{ secrets.APPSERVER_SSH_PORT }} - username: ${{ secrets.APPSERVER_SSH_USER }} - key: ${{ secrets.APPSERVER_SSH_PRIVATE_KEY }} - script: docker compose --project-directory crypter-web-container --profile ${{ env.docker_compose_profile }} pull + run: ssh appserver 'docker compose --project-directory crypter-web-container --profile ${{ env.docker_compose_profile }} pull' - name: Migrate database - uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5 - with: - host: ${{ secrets.APPSERVER_SSH_HOST }} - port: ${{ secrets.APPSERVER_SSH_PORT }} - username: ${{ secrets.APPSERVER_SSH_USER }} - key: ${{ secrets.APPSERVER_SSH_PRIVATE_KEY }} - script: docker compose --project-directory crypter-web-container --profile ${{ env.docker_compose_profile }} run api /app/efbundle + run: ssh appserver 'docker compose --project-directory crypter-web-container --profile ${{ env.docker_compose_profile }} run api /app/efbundle' - name: Start service - uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5 - with: - host: ${{ secrets.APPSERVER_SSH_HOST }} - port: ${{ secrets.APPSERVER_SSH_PORT }} - username: ${{ secrets.APPSERVER_SSH_USER }} - key: ${{ secrets.APPSERVER_SSH_PRIVATE_KEY }} - script: systemctl --user start crypter.service + run: ssh appserver 'systemctl --user start crypter.service' diff --git a/Documentation/Production/Server Setup/Web Server Setup.md b/Documentation/Production/Server Setup/Web Server Setup.md index eed620913..5b1c1cdf8 100644 --- a/Documentation/Production/Server Setup/Web Server Setup.md +++ b/Documentation/Production/Server Setup/Web Server Setup.md @@ -14,6 +14,34 @@ Create an SSH user and add corresponding details to the environment secrets with The user will need permissions to Docker, so add the user to the `docker` group. +## Record the host key + +The deploy workflow verifies the host key of the server it connects to, so record that key while the server is being set up. + +The secret holds `known_hosts` lines exactly as ssh writes them. The host field has to match `APPSERVER_SSH_HOST` and `APPSERVER_SSH_PORT`: a bare hostname on port 22, and `[host]:port` on any other port. A line recorded under a different name or port is never consulted, so the deploy fails as though no key had been recorded at all. + +Take the key from a workstation that already connects to the server, which by this point is whichever one was used to set it up. Print the entry it trusts: + +```bash +ssh-keygen -F '[]:' -f ~/.ssh/known_hosts +``` + +Drop the brackets and the port if that workstation connects over port 22. An entry that already carries the port the deploy uses can go straight into the secret, ignoring the leading comment line. + +An entry recorded under any other port has to be recaptured under the right one, then checked against the entry already trusted: + +```bash +ssh-keyscan -t -p > known_hosts +ssh-keygen -lf known_hosts +ssh-keygen -F '' -f ~/.ssh/known_hosts | ssh-keygen -lf - +``` + +The fingerprints must match. Comparing them is what makes the scan trustworthy, because `ssh-keyscan` on its own only reports whatever answers on the network. Pass `-t` for the key type that was checked, so nothing unverified lands in the secret. + +Add the contents of `known_hosts` to the environment secrets as `APPSERVER_SSH_KNOWN_HOSTS`. Every environment has its own server and its own host key, so record one for each. + +Rebuilding a server generates a new host key. Deploys fail with `REMOTE HOST IDENTIFICATION HAS CHANGED` until the secret is updated to match. + ## Copy the .env file Locate the `.env` file at the root of this repository, [here](../../../.env).