Security: The Habits That Matter Most
I confess: When writing code, my mind focuses on functionality, clarity, maintainability, and correctness. However, code that is “correct” (as in: working according to the specification) isn't necessarily secure. There are many aspects of security that don't come automatically through the absence of bugs (where “bug” is defined as “not working as specified”). Security measures need to be intentionally added to the code and to the development process (in form of tools, steps, and conventions).
What are practical steps to take? Focus on three aspects: (1) applying security measures to the code, (2) making use of tools and services, and (3) taking care of supply chain security.
Vulnerabilities are like the mythological nine-headed Hydra: For every head chopped off, the Hydra would regrow two heads. Here is a nine-fold countermeasure—three lists, each containing three security measures.
List 1: Top 3 Security Measures to Add to Code
1. Input Validation & Sanitization
User input is usually harmless until it isn't.
- Validate and sanitize user inputs to prevent injection attacks (SQL injection, XSS, etc.)
- Use Go’s
html/template
(rather thantext/template
) package for HTML templating, to automatically escape HTML characters
2. Secure Authentication & Authorization
Authentication and authorization are popular targets for attackers, so the most important option here, especially if you're inexperienced, is to reach out to tested-and-proven libraries and services to handle user management and resource access.
I have no particular recommendation, but the open source projects ZITADEL and Ory Kratos seem to be popular and beginners-friendly.
3. Concurrency Safety
Concurrent code has many more moving parts compared to equivalent serial code. Attackers can, for example, exploit data races or goroutine leaks to run Denial-of-Service (DoS) attacks.
- Use Go’s race detector (
go test -race
) to identify race conditions. Attackers could exploit some forms of race conditions to manipulate data - Eliminate Time-of-Check-to-Time-of-Use (TOCTOU) races where an attacker can exploit the gap between validation of a conditional access and the actual access of data.
List 2: Top 3 Security Tools for Go Development
1. Static Analysis Tools
Static code analysis can reveal quite a few security-related problems. At a minimum, use these two tools:
- govulncheck: Scans for vulnerabilities in dependencies and binaries
- gosec: Detects insecure code patterns (such as weak RNG or TLS misconfigurations). Also available as a linter; see the following list item.
2. Linters
Linters check code quality based on a wide range of (objective and subjective) criteria, including security.
Using the meta-linter golangci-lint is probably a no-brainer among gophers already, but ensure that security-releated linters are actually enabled in your environments. The staticcheck
and gosec
linters are a good start.
3. Fuzz Testing
Hand-written unit tests can unintentionally omit rare but critical edge cases that attackers can exploit.
Fuzz testing auto-generates input to uncover such edge-case vulnerabilities that static test cases might have missed.
List 3: Top 3 Measures for Supply Chain Security in Go
1. Dependency Management
Go's module management is conservative about module updates, which is a great plus for security. The Minimum Version Selection algorithm picks the most minimal version possible of a module. Updates to newer versions only happen through human intervention, such as updating dependencies (via go get -u
), optimally only after reviewing and testing newer versions of a module.
2. Immutable Proxies & Checksums
The Go module proxy not only accelerates module downloads but also ensures that a given version of a module, after being published and downloaded for the first time, cannot be tampered with. It is enabled by default, but for specific requirements, check the documentation to learn how to disable the proxy for company-internal module servers or run your own, private module proxy.
3. Minimalism & Isolation
Go fosters minimalist development and deployment in many ways, compiling to a single, static binary being one of them. To further reduce dependencies, consider using sandboxes such as containers or unikernels. The latter offer much higher security than containers do by default, as they bake a single Go executable and a virtual OS layer into a highly isolated micro-VM. Take a look at gokrazy for appliances or Unikraft (note the “k”) for unikernel VMs that can replace a container setup.
This list of lists is only the tip of the iceberg, though. It's meant for getting a foot in the door, but frankly, that door is heavy, and to swing it fully open, you have to dig much deeper.
Here are a few resources to get started:
- The OWASP Go Secure Coding Practices Guide: This eBook is the Go version of a secure coding practice reference guide by the Open Worldwide Application Security Project (OWASP). It covers security measures from input validation to cryptographic practices. A must-read.
- Golang Security Best Practices by Ahmad Sadeddin: This long blog post covers many of the topics from the OWASP guide in a concise format—best for getting a first overview of what's necessary to put up the shields.
- Supply Chain Security with Go by Michael Stapelberg: A talk held at the 22nd “Gulaschprogrammiernacht” (“goulash programming night”). Current Go versions have strong support for supply chain security, where the term supply chain includes the Go toolchain itself, 3rd-party modules, and your code.
- Writing Secure Go Code by Jakub Jarosz: A quick-start guide to using security tools. If you feel you have no time for reading the eBook or the long articles above, start here.
- Securing Credentials in Golang With Systemd: Where to securely store credentials and other secrets in a mininal setup? Instead of using standard solutions like Vault or etcd, Stephan Schmidt suggests to use what's already there (on a typical Linux system): The system and service manager
systemd
.
Stay secure!