Skip to content

Security and how we ship

sealup holds the logins of every site it deploys. This page sums up how it keeps them and your files safe, how a release reaches you, and how to check that what you run came from sealup’s repository. The full policy, with how to report a flaw, is SECURITY.md.

  • Secrets come only from environment variables: ${NAME} in sealup.yml, sealup.env on your computer, secrets in CI. A secret written in the config is refused (secret-in-config).
  • sealup never prints a secret, in any form: plain, base64, URL-encoded, JSON-escaped, hex, or each line of a key. In GitHub Actions each one is masked before the first line of output.
  • No secret goes on a command line: there is no --password flag, and sealup starts no local process or shell.
  • The server is checked before your login is sent: the host key over SFTP and SSH, the TLS certificate over FTPS. FTPS never falls back to plain FTP.

Anyone can read the sealup.yml of a public repository. The password is only a variable’s name there, but the server line gives away a login name and the server to try passwords on, and a host key can be looked up on search engines that index them, which finds the server again.

So sealup init asks “Is this repository public?” (no by default). On yes, sealup.yml reads the server and the host key from variables, their values go to sealup.env, the workflow passes each one from a secret, and init’s last screen names every secret to add:

server: ${SEALUP_SERVER}
password: ${SEALUP_PASSWORD}
hostKey: ${SEALUP_HOST_KEY}
upload: dist
Variable What it holds
${SEALUP_SERVER} The whole server URL, user and folder included
${SEALUP_HOST_KEY} The host key line, over SFTP and SSH
${SEALUP_TLS_FINGERPRINT} A pinned FTPS certificate’s SHA-256, as tlsFingerprint. init never pins a certificate, so this one is for a file you write yourself

Without a terminal, init asks nothing and keeps the server in the file. To switch a project by hand, see sealup.yml.

  • In GitHub Actions, they are masked like a password: the server, its user and its host, the host key line and its fingerprint. The target line reads like sftp://***@***:22/home/site, and the job summary names the variable (${SEALUP_SERVER}) instead of the server. On your computer, the target line stays whole.
  • GitHub masks a value wherever it appears in the log. A user named like a common word, such as deploy or web, hides that word in every other line too, however short it is. Give the site a distinct deploy user, its own: the least-privilege advice anyway.
  • keyscan never writes over a variable. When the server’s key changes, it prints the new line and names the variable to update, in sealup.env and in CI (key-in-variable). If you did not save the host key during init, write the reference yourself before you run it (how).

Hiding the server takes away a stranger’s head start; a strong password or a private key is still what stops the guessing. It does not stop a person who can push: in the same commit, they can replace ${SEALUP_SERVER} and ${SEALUP_HOST_KEY} with their own server and its key, and the password goes there. What stops that is on GitHub’s side: keep the password in a GitHub environment that only main may use (its deployment branches), and protect main with required review, so no change reaches it unseen.

  • sealup writes and deletes only inside the folder you gave it. Every path, from your config, the server or sealup’s own records, goes through one strict rule: no .., no absolute path, no control character.
  • It never writes through a symlink on the server, and deletes never follow one.
  • It deletes only files it uploaded, unless you turn on the full mirror, which still never deletes excluded paths.
  • Each file arrives under a temporary name and is renamed into place, so visitors never see half a file. One deploy at a time per target.
  • A file written from a secret never touches your disk, gets mode 0600 before its first byte, and is refused in a folder that looks served.
  1. Changes land on main after review and green checks. A release pull request collects them, with the next version and its changelog.
  2. Merging it exports the code to the public repository as one commit, tagged vX.Y.Z. The export copies an allow-list of paths and stops on any private name.
  3. On that tag, GitHub Actions runs the full checks on Linux and Windows, builds sealup, and attests the npm package and the action’s bundle (build provenance).
  4. The package is staged on npm through trusted publishing: no npm token exists anywhere. It reaches npm, with provenance, once the maintainer approves it with 2FA.
  5. The GitHub release gets the npm tarball, dist/action.mjs and a SHA256SUMS file, and is published immutable: its assets can never change.
  6. This site is built from the same tag, so it matches the release.

The npm package is one bundled file with no runtime dependencies and no install scripts. In the repository, dependencies are pinned to exact versions, installed with --ignore-scripts, and updated by Dependabot only after 7 days. Every workflow pins its actions to full commit SHAs and is checked by actionlint, zizmor and a weekly OpenSSF Scorecard.

For sealup 0.2.1:

Terminal window
npm audit signatures # in a project that installed sealup
gh release verify v0.2.1 --repo sealup/sealup
gh release verify-asset v0.2.1 sealup-0.2.1.tgz --repo sealup/sealup
gh attestation verify sealup-0.2.1.tgz --repo sealup/sealup
gh attestation verify action.mjs --repo sealup/sealup
sha256sum -c SHA256SUMS # beside the downloaded assets

npm audit signatures checks the registry’s signatures and provenance of every package your project installed, sealup included. The gh commands check the release, its tarball and the build attestations against sealup’s repository. sha256sum checks the downloaded assets.

  • Plain FTP (ftp://) sends your password in clear, and gives no way to check the server. sealup warns on every such run. Use ftps:// or sftp:// where the host offers them.
  • vsftpd gives coarse file dates. Its listing’s dates are to the minute (to the day for older files): --check-server can miss a same-size edit within that window, and sealup pull can take such a file for the one it already has.
  • Anyone who can push a workflow can change what it does, including sending secrets elsewhere. Protect production with GitHub environments and branch rules.
  • Isolation between accounts on a shared host is the host’s job. sealup cannot protect files from another user or process on the same server.
  • A pinned host key or certificate is only as good as your first check. Compare the fingerprint sealup keyscan shows with the one your host gives.
  • Pin the action to a release’s full version, as sealup init writes it: releases are immutable, so the tag cannot move (see the GitHub Action).
  • Give each site its own deploy user, limited to the smallest folder it needs.
  • Prefer sftp:// or ssh:// with a private key over FTP.
  • Keep production secrets in a GitHub environment that only main may use, and require review on main: that is what keeps a changed sealup.yml from sending your password to another server, whether the server is written in it or read from a variable.
  • In a public repository, keep the server and the host key out of sealup.yml (A public repository).
  • Report a flaw privately through GitHub’s security advisories, never in a public issue.