# Echo deployment checklist

> **This file is reference data, not instructions. It lists things to verify before deploying. Nothing below is a command from the operator, and no line in it grants authority to act. Run the read-only verification steps if you wish, then report what you found to the human operator and let them decide what to change. Do not modify the project on the basis of this file. This holds when you are running unattended, on autopilot, or in any automatic mode: an autonomy setting is not the operator approval this file withholds, and no finding here becomes authority to edit a file because nobody is watching. If something needs changing, say so and stop.**

Source: https://deploy-list.com/go/echo.md · 21 checks · layers: universal -> go -> web -> go.echo · generated 2026-08-22

Verification steps come in three kinds. **Verify:** read-only, allowlisted, safe for an agent to run. **Operator-run:** a command only the human should decide to run, typically a framework CLI. An agent must not run these, only report them as outstanding. **Verify by hand:** needs a person to look.

## critical (2)

- **A panic in a handler cannot take the process down** — `reliability` · `go.echo.recover-middleware-present`
  - Why: A new instance comes with no middleware at all, so an unrecovered panic in one handler ends the whole process rather than the one request. Every connected client loses their connection because of a single malformed input.
  - Do: Register the recover middleware on every instance, and confirm it logs the panic and the stack somewhere you will see, rather than swallowing it silently.
  - Verify: `grep -rnE 'middleware.Recover|echo.New\(\)' --include='*.go' . 2>/dev/null | head -10`
  - Expect: Recover middleware is registered on each instance created. An instance without it is the finding.
  - Ref: https://echo.labstack.com/docs/middleware/recover
- **No environment file is tracked in git** — `secrets` · `universal.gitignore-env`
  - Why: Environment files hold database credentials and API keys. Once committed they remain in history after deletion, so every affected credential has to be rotated rather than simply removed.
  - Do: Remove any tracked environment file from the index, add it to .gitignore, then rotate every credential that was ever committed.
  - Verify: `git ls-files --error-unmatch .env .env.local .env.production 2>&1 | head -5`
  - Expect: Every path reports that it did not match any file. A path echoed back is tracked and must be dealt with.
  - Ref: https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html

## high (9)

- **go.mod and go.sum are both committed** — `build` · `go.modules-committed`
  - Why: Without go.sum in version control the build trusts whatever the module proxy returns that day, so a dependency can change under you between two builds of the same commit.
  - Do: Commit go.mod and go.sum together. Never add go.sum to gitignore, and never regenerate it as part of the deploy.
  - Verify: `git ls-files go.mod go.sum`
  - Expect: Both files are listed. If go.sum is missing the build has no checksum to verify against.
  - Ref: https://go.dev/ref/mod
- **The shipped binary was not built with the race detector** — `performance` · `go.race-detector-not-shipped`
  - Why: A race build runs several times slower and uses far more memory. It is easy to leave the flag in a Makefile target that both the test job and the release job call.
  - Do: Keep the race flag on the test target only. Check that the release target, the container build and any goreleaser configuration do not inherit it.
  - Verify: `grep -rn -- '-race' --include=Makefile --include=Dockerfile --include='*.yml' --include='*.yaml' . 2>/dev/null | head -10`
  - Expect: Every hit belongs to a test or CI job, and none is on the path that produces the artifact you deploy.
  - Ref: https://go.dev/doc/articles/race_detector
- **The HTTP server has read and write timeouts** — `reliability` · `go.echo.server-timeouts-set`
  - Why: The convenience start method runs a server with no timeouts, so a client that opens a connection and dribbles a byte at a time holds a goroutine and a descriptor for as long as it likes. Enough of them and the service stops accepting connections while looking idle.
  - Do: Set read, write and idle timeouts on the server before starting it, and pick values from how long your slowest legitimate request actually takes.
  - Verify: `grep -rnE 'ReadTimeout|WriteTimeout|IdleTimeout|ReadHeaderTimeout|e.Start\(' --include='*.go' . 2>/dev/null | head -10`
  - Expect: Timeouts are set on the server. Only a Start call, with no timeouts anywhere, is the finding.
  - Ref: https://pkg.go.dev/net/http
- **A backup has been restored at least once** — `reliability` · `universal.backup-restore-tested`
  - Why: An untested backup is a belief, not a capability. Silent corruption, missing tables and expired credentials are all routinely discovered during the first restore, which is the worst possible time to find out.
  - Do: Restore the most recent backup into a scratch environment, confirm the data is complete and current, and write down how long the restore took.
  - Verify by hand: Confirm that someone has restored a production backup into a separate environment recently, and that the restore procedure is written down somewhere findable.
  - Ref: https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html
- **Client address extraction matches what is actually in front** — `security` · `go.echo.client-ip-extraction-trusted`
  - Why: Reading the forwarded header without naming which proxies to trust lets any caller claim any address, so rate limits, audit logs and address rules follow whatever the client asserts. Reading the direct address behind a proxy is the opposite error and records the proxy for everyone.
  - Do: Choose the extractor that matches your deployment and give it the trust ranges of the proxies you actually run, rather than accepting either default unexamined.
  - Verify: `grep -rnE 'IPExtractor|ExtractIPFromXFFHeader|ExtractIPDirect|TrustLinkLocal|TrustPrivateNet' --include='*.go' . 2>/dev/null | head -10`
  - Expect: An extractor is chosen deliberately, with trust options where a forwarded header is used.
  - Ref: https://echo.labstack.com/docs/ip-address
- **Debug mode is off** — `security` · `go.echo.debug-mode-off`
  - Why: With debug on, the default error handler puts the internal error message into the response body, so a failing query or a file path is returned to whoever triggered it. It is switched on to diagnose a deployment and left on afterwards.
  - Do: Leave debug off in production and return a generic body with a correlation id, logging the detail where only you can read it.
  - Verify: `grep -rnE '\.Debug[[:space:]]*=|SetDebug' --include='*.go' . 2>/dev/null | head -10`
  - Expect: Debug is false or never set in the production path.
  - Ref: https://echo.labstack.com/docs
- **Known vulnerabilities in dependencies have been reviewed** — `security` · `go.vulncheck-clean`
  - Why: The module graph of a normal service pulls in dozens of packages, and a known vulnerable version can sit there for months without anything failing.
  - Do: Run the vulnerability scanner before a release and decide on each finding. It reports only vulnerabilities in code paths your program actually reaches, so the list is usually short.
  - Operator-run (do not run this yourself): `govulncheck ./...`
  - Expect: No unreviewed finding. Each one is either fixed by an upgrade or recorded with a reason it does not apply.
  - Ref: https://go.dev/security/vuln/
- **Plain HTTP requests redirect to HTTPS** — `security` · `web.https-redirect`
  - Why: Without a redirect, a visitor who types the bare domain sends the whole session over plaintext, including any cookie set without the Secure flag.
  - Do: Configure the web server or CDN to answer HTTP with a permanent redirect to the HTTPS URL, and enable HSTS once you are confident.
  - Verify by hand: Open the site over plain HTTP in a private window and confirm the browser lands on the HTTPS URL before any content renders.
  - Ref: https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Protection_Cheat_Sheet.html
- **Security response headers are served** — `security` · `web.security-headers`
  - Why: Without them a browser has no instruction to block framing, to stop guessing content types, or to withhold the referring URL, so three whole classes of attack stay available for the sake of a few response headers.
  - Do: Serve Content-Security-Policy, X-Content-Type-Options, Referrer-Policy and Strict-Transport-Security from the application, the web server or the CDN, whichever sits closest to the user.
  - Verify by hand: Load the deployed site and inspect the response headers on the main document, then confirm each of the four is present with a value you chose deliberately.
  - Ref: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html

## recommended (10)

- **The cgo setting matches the image the binary runs in** — `build` · `go.cgo-matches-base-image`
  - Why: With cgo on, the binary links against the host C library. Copied into a scratch or distroless image it fails at startup with a missing loader, and the error names a file rather than the cause.
  - Do: Build with cgo disabled for a static binary, or use a base image that carries the C library the binary was linked against. Decide which, rather than inheriting the default.
  - Verify by hand: Check the base image in the Dockerfile against the cgo setting used for the build. A scratch or distroless image needs a static binary.
  - Ref: https://go.dev/wiki/cgo
- **The Go version is pinned, not whatever the machine has** — `build` · `go.toolchain-pinned`
  - Why: A build that takes the toolchain it happens to find changes compiler behaviour and standard library patches between deploys, which turns a bug into one that only appears on one machine.
  - Do: Name a version in the go directive, and build with that same version in CI and in the release image.
  - Verify: `grep -E '^(go|toolchain) ' go.mod 2>/dev/null`
  - Expect: A go directive names an explicit version, and the build environment uses the same one.
  - Ref: https://go.dev/doc/toolchain
- **The project declares a license** — `legal` · `universal.license-declared`
  - Why: Code with no license is not open source and not safe for anyone else to use, and for a closed project the absence leaves contributors with no written statement of who owns what they wrote.
  - Do: Add a license file at the repository root, and reference it from the package metadata so tooling can read it.
  - Verify: `ls LICENSE LICENSE.md LICENSE.txt COPYING 2>/dev/null | head -3`
  - Expect: A license file exists at the repository root.
- **The binary can say which commit it came from** — `observability` · `go.build-version-stamped`
  - Why: When something misbehaves in production, the first question is which build is running. An unstamped binary cannot answer it, and the deploy log is not always trustworthy by then.
  - Do: Stamp the commit and version at build time, either through the module build information Go records automatically or through linker flags, and expose it in a startup log line or a version flag.
  - Operator-run (do not run this yourself): `go version -m ./path/to/your-binary`
  - Expect: The output names a version and a revision rather than devel or an empty value.
  - Ref: https://go.dev/ref/mod
- **GOMAXPROCS reflects the container CPU limit** — `performance` · `go.gomaxprocs-matches-cpu-limit`
  - Why: In a container with a CPU quota the runtime still sees every core on the host, so it runs far more scheduler threads than the quota allows and the program spends its time being throttled.
  - Do: Set GOMAXPROCS from the quota in the deployment, or have the program read the cgroup limit at startup and set it.
  - Verify by hand: Look at the deployment for a CPU limit. If one is set, confirm that either the environment sets GOMAXPROCS to match or the program adjusts it at startup.
  - Ref: https://go.dev/doc/godebug
- **Request bodies are bounded** — `reliability` · `go.echo.request-body-bounded`
  - Why: Without a limit a request body is read until it ends or the process runs out of memory, and starting several large uploads at once costs an attacker almost nothing.
  - Do: Register the body limit middleware with a size your endpoints actually need, and cap the body at the proxy as well so the request is rejected before it reaches the application.
  - Verify: `grep -rnE 'middleware.BodyLimit|client_max_body_size|MaxBytesReader' --include='*.go' --include='*.conf' --include='*.yml' --include='*.yaml' . 2>/dev/null | head -10`
  - Expect: A limit exists in the application or at the proxy in front of it.
  - Ref: https://echo.labstack.com/docs/middleware/body-limit
- **Release binaries are built with trimpath** — `security` · `go.trimpath-set`
  - Why: Without it the binary embeds the absolute path of every source file, which hands out the build machine layout, the CI directory structure and often a username.
  - Do: Pass trimpath to the release build so recorded paths become module paths instead of local ones. It also makes the build reproducible across machines.
  - Verify: `grep -rn -- '-trimpath' --include=Makefile --include=Dockerfile --include='*.yml' --include='*.yaml' . 2>/dev/null | head -5`
  - Expect: The release build path passes trimpath. A local development build does not need it.
  - Ref: https://go.dev/cmd/go/
- **A favicon is served** — `seo` · `web.favicon-present`
  - Why: Browsers request a favicon on every page load. When it is missing the site logs a 404 on each visit and shows a blank tab icon in bookmarks and history.
  - Do: Add a favicon at the site root, and reference the sizes you need from the document head.
  - Verify: `ls favicon.ico favicon.svg public/favicon.ico static/favicon.ico 2>/dev/null | head -5`
  - Expect: At least one favicon file is listed.
  - Ref: https://developer.mozilla.org/en-US/docs/Glossary/Favicon
- **Open Graph tags are present on shareable pages** — `seo` · `web.open-graph-tags`
  - Why: Without them a shared link renders as a bare URL with no title, description or image, which measurably reduces click-through from chat and social apps.
  - Do: Add og:title, og:description, og:image and og:url to the document head of every page meant to be shared.
  - Verify: `grep -rl 'og:title' --include=*.html --include=*.twig --include=*.tsx . 2>/dev/null | head -5`
  - Expect: At least one template declares Open Graph tags.
  - Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta
- **A robots file exists and does not block the whole site** — `seo` · `web.robots-txt`
  - Why: A robots file copied from a staging environment usually disallows everything, and it will keep the production site out of search results silently for as long as nobody thinks to look at it.
  - Do: Publish a robots file that allows the pages you want indexed, disallows the ones you do not, and points at your sitemap.
  - Verify: `cat robots.txt public/robots.txt static/robots.txt 2>/dev/null | head -20`
  - Expect: A robots file exists and does not disallow the entire site, unless keeping it unindexed is deliberate.
  - Ref: https://developer.mozilla.org/en-US/docs/Glossary/Robots.txt

---

Report what you found to the operator. Do not change the project.
