| name | rust-security |
| description | | Use when this capability is needed. |
Rust Security
Practical security guidance for Rust services and libraries. Rust removes many memory-safety bugs, but it does not remove auth bugs, logic bugs, injection, weak crypto, dependency risk, or unsafe-code soundness obligations.
Quick Navigation
- references/crypto_secrets.md - password hashing, tokens, TLS, key handling, zeroization
- references/supply_chain.md - cargo audit, cargo deny, dependency policy, SBOMs
Golden Rules
- Treat all external input as hostile until parsed into domain types.
- Use reviewed crypto crates and protocols; do not design cryptography.
- Keep secrets out of
Debug, logs, panics, metrics, and error contexts.
- Bound work for untrusted requests: size, depth, time, concurrency, and retries.
- Automate dependency and license policy checks in CI.
Threat Model First
asset: session token
attacker: internet client with many accounts
entry points: login, refresh, logout, API auth middleware
controls: TLS, secure cookies, token rotation, rate limits, audit logs
failure modes: replay, theft from logs, weak signing key, missing tenant check
A short threat model beats scattered hardening. Identify assets, attackers, boundaries, and abuse cases before choosing crates.
Secrets Handling
use secrecy::{ExposeSecret, SecretString};
pub struct Config {
database_url: SecretString,
}
async fn connect(config: &Config) -> Result<(), sqlx::Error> {
sqlx::PgPool::connect(config.database_url.expose_secret()).await?;
Ok(())
}
Use secret wrapper types to avoid accidental Debug output. Avoid attaching raw secrets to errors with .context().
Memory Zeroization
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
let mut key = Zeroizing::new([0u8; 32]);
key.copy_from_slice(&raw_key);
impl Zeroize for SessionKey {
fn zeroize(&mut self) {
self.inner.zeroize();
self.created_at.zeroize();
}
}
Use zeroize for cryptographic material. Relying on the OS to reclaim memory is not sufficient.
Password Hashing
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use password_hash::{rand_core::OsRng, SaltString};
pub fn hash_password(password: &[u8]) -> Result<String, password_hash::Error> {
let salt = SaltString::generate(&mut OsRng);
Ok(Argon2::default().hash_password(password, &salt)?.to_string())
}
pub fn verify_password(password: &[u8], encoded: &str) -> Result<bool, password_hash::Error> {
let parsed = PasswordHash::new(encoded)?;
Ok(Argon2::default().verify_password(password, &parsed).is_ok())
}
Use password hashing algorithms for passwords, not fast hashes. Tune cost parameters for the deployment budget.
JWT Verification
use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm, TokenData};
#[derive(Debug, serde::Deserialize)]
pub struct Claims {
pub sub: String,
pub exp: usize,
pub iss: String,
pub aud: String,
}
pub fn verify_token(token: &str, public_key: &[u8]) -> Result<Claims, jsonwebtoken::errors::Error> {
let mut validation = Validation::new(Algorithm::RS256);
validation.set_audience(&["my-service"]);
validation.iss = Some("auth.my-service.com".to_string());
let token_data: TokenData<Claims> = decode(
token,
&DecodingKey::from_rsa_pem(public_key)?,
&validation,
)?;
Ok(token_data.claims)
}
Always validate exp, iss, and aud. Never accept alg: "none". Use asymmetric keys (RS256/ES256) so services verify without holding signing keys.
TLS Configuration
use rustls::ClientConfig;
use rustls_platform_verifier::tls_config;
pub fn tls_client() -> ClientConfig {
tls_config()
.with_client_auth(none)
.expect("tls config")
}
Pin a minimum TLS version (1.2 or 1.3) and restrict cipher suites to modern options.
Parse, Validate, Authorize
#[derive(Debug, Clone)]
pub struct TenantId(uuid::Uuid);
pub async fn load_invoice(
auth: &AuthContext,
tenant_id: TenantId,
invoice_id: InvoiceId,
) -> Result<Invoice, Error> {
auth.require_tenant(&tenant_id)?;
repo::find_invoice(tenant_id, invoice_id).await?
.ok_or(Error::NotFound)
}
Validate shape at the boundary, then authorize with domain identifiers. Do not rely on UI filtering or hidden route parameters.
Auth Middleware Pattern
use axum::{
extract::{FromRequestParts, Request},
middleware::{from_fn, Next},
response::Response,
};
pub async fn auth_middleware(
mut req: Request,
next: Next,
) -> Result<Response, StatusCode> {
let token = req.headers()
.get("Authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.ok_or(StatusCode::UNAUTHORIZED)?;
let claims = verify_token(token, &PUBLIC_KEY)
.map_err(|_| StatusCode::UNAUTHORIZED)?;
req.extensions_mut().insert(AuthUser {
user_id: claims.sub,
tenant_id: claims.aud,
});
Ok(next.run(req).await)
}
Rate Limiting
use governor::{DefaultDirectRateLimiter, Quota, RateLimiter};
use nonzero_ext::nonzero;
use std::num::NonZeroU32;
let limiter = RateLimiter::direct(Quota::per_second(
NonZeroU32::new(100).unwrap(),
));
async fn handle_request() -> Result<(), StatusCode> {
if limiter.check().is_err() {
return Err(StatusCode::TOO_MANY_REQUESTS);
}
Ok(())
}
Use token-bucket or sliding-window ratelimiters. Expose rate limit headers (X-RateLimit-Remaining, Retry-After) for client cooperation.
Input Validation with Bounds
use validator::{Validate, ValidationError};
#[derive(Debug, Validate, serde::Deserialize)]
pub struct SignupRequest {
#[validate(length(min = 3, max = 100))]
pub username: String,
#[validate(email)]
pub email: String,
#[validate(length(min = 8, max = 256))]
pub password: String,
}
Validate at the boundary with explicit constraints. Combine with size limits at the transport layer.
Untrusted Input Bounds
pub fn parse_payload(bytes: &[u8]) -> Result<Event, Error> {
if bytes.len() > 64 * 1024 {
return Err(Error::PayloadTooLarge);
}
let value: serde_json::Value = serde_json::from_slice(bytes)?;
parse_event(value)
}
Bound payload sizes, recursion depth, decompression output, regex complexity, and database result sizes.
SSRF Protection
use url::Url;
pub fn validate_outbound_url(raw: &str) -> Result<Url, Error> {
let url = Url::parse(raw)?;
let scheme = url.scheme();
if scheme != "https" && scheme != "http" {
return Err(Error::DisallowedScheme(scheme.to_string()));
}
let host = url.host_str().ok_or(Error::MissingHost)?;
if is_private_ip(host) {
return Err(Error::BlockedPrivateHost(host.to_string()));
}
Ok(url)
}
fn is_private_ip(host: &str) -> bool {
false
}
Prevent SSRF by filtering private IPs, loopback, and metadata endpoints before making outbound HTTP calls.
Path Safety
use camino::{Utf8Path, Utf8PathBuf};
pub fn resolve_upload(root: &Utf8Path, name: &str) -> Result<Utf8PathBuf, Error> {
if name.contains('/') || name.contains('\\') || name == "." || name == ".." {
return Err(Error::InvalidPath);
}
Ok(root.join(name))
}
Do not join user-provided paths directly. Prefer opaque IDs and server-generated filenames.
Dependency Supply Chain
cargo audit
cargo deny check
cargo sbom --output cyclonedx --format json
[advisories]
vulnerability = "deny"
unmaintained = "warn"
[licenses]
allow = ["MIT", "Apache-2.0", "ISC", "BSD-3-Clause"]
deny = ["GPL-3.0", "AGPL-3.0"]
[bans]
multiple-versions = "deny"
wildcards = "deny"
deny = ["ansi_term"]
Anti-Patterns
let digest = sha2::Sha256::digest(password);
let hash = Argon2::default().hash_password(password, &salt)?;
#[derive(Debug)]
struct Login { password: String }
struct Login { password: secrecy::SecretString }
let claims: Claims = decode_header(token).into();
let token = decode::<Claims>(token, &key, &Validation::new(Algorithm::RS256))?;
if input_token == stored_token { }
use subtle::ConstantTimeEq;
let result = input_token.as_bytes().ct_eq(stored_token.as_bytes());
Security Checklist
- Threat model names assets, attackers, trust boundaries, and abuse cases.
- Secrets use wrapper types and are excluded from logs/errors/debug output.
- Passwords use Argon2id/bcrypt/scrypt with per-password salts.
- Tokens have expiration, audience/issuer checks, key rotation plan, and replay controls.
- Untrusted input has explicit size/time/depth bounds.
- Authorization checks use server-side domain identifiers.
- CI runs dependency, license, and advisory checks.
- TLS minimum version 1.2, cipher suites restricted to modern set.
- Rate limiting is applied to authentication and resource-intensive endpoints.
- SSRF protection is in place for all outbound HTTP clients.
- Cryptographic keys use zeroize for memory cleanup.
References
Source: adxptived/Rust-Skills — distributed by TomeVault.