Go Security Best Practices: Hardening Your Backend

Go Security Best Practices: Hardening Your Backend
By default, Go is a very secure language. It is memory-safe, making it resistant to the buffer overflow attacks that plague C and C++. However, the web is a hostile environment, and a secure language doesn't guarantee a secure application.
In this penultimate module, we will explore the essential security controls every Go developer must implement to protect their users and their data.
Go Security Best Practices at a Glance
Go backend security requires four key layers: preventing injection attacks with parameterized queries, enforcing strict HTTP security headers via middleware, hashing passwords with BCrypt or Argon2, and running automated static analysis with gosec. Layering these controls creates a defence-in-depth posture aligned with the OWASP Top 10.
Security by Design
Preventing Injection
SQL Injection is the #1 cause of data breaches. Never concatenate strings to build a SQL query. Instead, always use parameterized queries with the ? or $1 placeholders.
Security Headers
Use middleware (like we learned in Module 22) to set essential security headers on every response. This helps prevent Cross-Site Scripting (XSS) and Clickjacking.
Static Analysis with gosec
gosec is an open-source tool that scans your Go source code for security problems. It can find hardcoded credentials, unsafe use of cryptography, and much more.
Critical Security Controls
golang.org/x/crypto/bcryptNever store passwords in plain text. Use BCrypt with a cost factor of at least 10 for industry-standard hashing.
HttpOnly, Secure, SameSiteSet these flags on your session cookies to prevent them from being stolen via XSS or CSRF.
go list -m allRegularly audit your go.mod file for outdated or vulnerable second-party libraries using 'govulncheck'.
golang.org/x/time/ratePrevent brute-force attacks on your login endpoints by limiting how many requests an IP can make.
| Task / Feature | Standard Development | Secure Development |
|---|---|---|
| Passwords | Stored in MD5 or SHA1 (Vulnerable) | Hashed with BCrypt or Argon2 (Secure) |
| Queries | Dynamic strings (SQL Injection risk) | Parameterized placeholders (Safe) |
| Headers | Browser defaults | Strict CSP and Security headers |
Protecting Against CSRF
Cross-Site Request Forgery (CSRF) attacks trick authenticated users into submitting requests they did not intend to make. Protect your state-changing endpoints by validating a CSRF token on every non-GET request.
On your HTML forms or API clients, include the CSRF token in the X-CSRF-Token header. Single-page applications using httpOnly cookies are naturally protected from CSRF when using the SameSite=Strict cookie attribute.
Securing Secret Management
Never hardcode API keys, database passwords, or cryptographic secrets in your source code. Use environment variables loaded at startup and rotate them regularly.
For production deployments on Kubernetes or AWS, use a secrets manager (AWS Secrets Manager, HashiCorp Vault, or Kubernetes Secrets) rather than .env files, which can be accidentally committed. This is especially relevant when following the Go deployment with Docker and CI/CD guide.
Input Sanitisation and Validation
Complement parameterized queries with strict input validation at the handler layer. Reject unexpected field lengths, formats, and character sets before data reaches your business logic.
This mirrors the validation pattern used in the Go REST API project guide, where every POST handler validates its payload before writing to the database.
TLS Configuration
Always terminate TLS at the Go server level for internal service communication. Go's crypto/tls package supports TLS 1.2+ by default, but you should explicitly enforce minimum version requirements in production.
The Go crypto/tls package documentation details all available cipher suites and configuration options.
Vulnerability Scanning with govulncheck
Beyond gosec for static analysis, the Go team ships govulncheck — a tool that checks your specific code paths against the Go vulnerability database. Unlike CVE scanners that flag every transitive dependency, govulncheck only reports vulnerabilities that your code actually calls.
Run this in your CI pipeline after every dependency update to catch newly disclosed vulnerabilities before they reach production.
For a broader view of how these security controls integrate with the OWASP API Security framework, see our How to protect APIs from attacks guide. Middleware-level security header injection is covered in detail in Go middleware patterns.
Common Mistakes and How to Avoid Them
Security vulnerabilities in Go backends often follow predictable patterns. These are the five most dangerous mistakes and how to prevent them.
1. Using math/rand for security-sensitive values. math/rand produces deterministic sequences that can be predicted if an attacker knows the seed. For session tokens, CSRF tokens, and password reset codes, always use crypto/rand. The Go crypto/rand package is non-deterministic and seeded by the OS.
2. Logging sensitive values. A common debugging shortcut is log.Printf("user: %+v", user) — which may print passwords, tokens, or PII to application logs. Use a structured logger like log/slog and define safe LogValue() methods on sensitive structs to control what gets logged.
3. Returning raw error messages to clients. Database errors, file-system errors, and internal panics often contain server topology details (table names, file paths, IP addresses). A client receiving pq: column "password_hash" does not exist learns something useful for an attack. Return a generic 500 message to clients and log the full error internally.
4. Not validating redirect URLs. OAuth flows and login pages often accept a redirect_to query parameter. Without validation, an attacker passes redirect_to=https://evil.com and your server redirects authenticated users there (open redirect). Always validate that the redirect target is on an allowlist of trusted domains.
5. Skipping dependency audits. Even if your own code is secure, a vulnerable transitive dependency can expose your users. Run govulncheck ./... in your CI pipeline after every dependency update to check your actual code paths against the Go vulnerability database. Unlike generic CVE scanners, govulncheck only flags vulnerabilities that your code actually calls.
FAQ
Q: Is BCrypt or Argon2 better for password hashing in Go?
Both are secure when configured correctly. BCrypt (via golang.org/x/crypto/bcrypt with cost ≥ 12) is simpler to use and well-audited. Argon2id (via golang.org/x/crypto/argon2) is newer, resistant to GPU attacks, and recommended by OWASP for new systems. For most applications, either is acceptable. The Go extended crypto packages provide implementations for both.
Q: How do I prevent timing attacks when comparing tokens?
Use subtle.ConstantTimeCompare from the crypto/subtle package instead of == for comparing secrets and tokens. Direct string comparison short-circuits on the first mismatch — an attacker can measure the response time to deduce correct prefix bytes. Constant-time comparison takes the same time regardless of where the mismatch occurs.
Q: Should I use JWTs for session management?
JWTs are stateless by design, which makes them difficult to revoke before expiry. For applications that need immediate logout (e.g., after a password change), opaque tokens stored in a database or Redis give you the ability to invalidate a session instantly. JWTs are best suited for short-lived, cross-service authentication tokens where revocation is not critical.
Next Steps
You have now mastered the foundations, architecture, and security of Go. There is only one thing left — to prove your knowledge! In our final module, we will present the Go Mastery Final Knowledge Test, where you can validate everything you've learned on this 27-part journey.
Common Go Security Mistakes
1. SQL injection via string concatenation
db.Query("SELECT * FROM users WHERE id = " + userInput) is vulnerable to SQL injection. Always use parameterised queries: db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1", userInput). See the OWASP SQL Injection guide.
2. Storing passwords in plaintext
Never store raw passwords. Use bcrypt via golang.org/x/crypto/bcrypt — bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost). Never use MD5 or SHA-1 for password hashing.
3. Hardcoding secrets in source code
API keys, database credentials, and JWT secrets in source code leak via version control history. Use environment variables or a secrets manager. The Go os.Getenv function is the minimal approach; tools like Vault or AWS Secrets Manager are production-grade solutions.
4. Missing input validation and size limits
HTTP handlers that read r.Body without http.MaxBytesReader are vulnerable to memory exhaustion attacks. Wrap: r.Body = http.MaxBytesReader(w, r.Body, 1<<20) (1MB limit) before reading the body.
5. Not setting security headers
Missing headers like X-Content-Type-Options, X-Frame-Options, and Content-Security-Policy expose the application to clickjacking and MIME-sniffing attacks. Add them in middleware using w.Header().Set(...).
Frequently Asked Questions
How do I generate a cryptographically secure random token in Go?
Use crypto/rand, not math/rand: token := make([]byte, 32); rand.Read(token). Encode to hex or base64 for use in URLs or headers. math/rand is deterministic and must never be used for security tokens.
How do I safely handle JWTs in Go?
Use a well-audited library like golang-jwt/jwt. Always validate the signature, check the exp claim, and use a strong secret (32+ bytes for HMAC-SHA256). Never decode a JWT without verifying the signature first.
What Go security tools should I run in CI?
Run govulncheck to check for known vulnerabilities in dependencies, gosec for static analysis of security issues, and go vet for general correctness issues. These three tools together catch the majority of common Go security issues before deployment.
