Showing posts with label devsecops. Show all posts
Showing posts with label devsecops. Show all posts

Wednesday, April 22, 2026

Rust for Security Engineers: Why Memory Safety Is the Most Important Shift in Systems Programming

Rust ownership and memory safety — abstract visualization of safe code boundaries

Introduction

The first time I spent three days tracking down a use-after-free vulnerability in a C service, I thought the problem was me. I had been careful. I had read the code. I had tested it. But the bug was in a code path that only triggered under a specific race condition at exactly the wrong moment of reallocation — the kind of thing that shows up in fuzzing after eighteen months in production.

The CVE was rated high severity. The fix was four lines. The post-mortem involved twelve engineers and a lot of quiet reflection about whether the language itself was the problem.

I've come to believe it was, at least partly. Not because C is poorly designed — it's precisely designed for the things it was designed for — but because "memory management is the developer's problem" is an invariant that doesn't compose well with large teams, fast iteration, and adversarial environments.

The security industry is starting to reach the same conclusion. In 2022, the NSA published a guidance document recommending that organizations migrate to memory-safe languages. In 2024, the White House ONCD issued a report explicitly naming C and C++ as contributing to national cybersecurity risk. Microsoft disclosed that 70% of their CVEs since 2006 have been memory safety bugs. Google's Android team found the same ratio in their vulnerability data.

Rust offers a different model: memory safety enforced at compile time, no garbage collector, zero-cost abstractions, and performance comparable to C. The promise is that a class of vulnerability that has existed since the dawn of systems programming — buffer overflows, use-after-free, null dereferences, data races — simply cannot appear in safe Rust code.

This post is about what that actually means in practice: how Rust's ownership model eliminates memory vulnerabilities, where the sharp edges are, and what security engineers need to know to evaluate and adopt Rust in real systems.


The Problem: Why Memory Bugs Keep Winning

Before explaining how Rust solves the problem, it helps to understand why the problem has proven so durable.

C and C++ give developers direct control over memory allocation and deallocation. You allocate a buffer, you write to it, you free it when you're done. This model is fast and flexible. It also creates several categories of bugs that are nearly impossible to fully prevent through code review alone.

Use-after-free: A pointer to freed memory is dereferenced later. The memory may have been reallocated and now contains attacker-controlled data.

Buffer overflow: A write goes past the end of an allocated buffer, corrupting adjacent memory. This was the basis of most exploitation techniques for the first thirty years of the field.

Double-free: A pointer is freed twice. This corrupts heap allocator metadata and is routinely exploitable.

Null dereference: A null pointer is dereferenced. Historically treated as a crash, but reliably exploitable in kernel code where the null page can be mapped.

Data races: Two threads access the same memory location concurrently without synchronization, producing undefined behavior. These show up intermittently and are notoriously hard to reproduce.

The difficulty isn't that developers don't know these bugs exist. Every C developer knows about use-after-free. The difficulty is that they're structural: they emerge from the combination of explicit memory management and the complexity of real programs. No amount of documentation, linting, or review catches them all. Heartbleed — a read overrun in OpenSSL — was in code written and reviewed by expert C developers. It existed for two years.

ASAN, Valgrind, and fuzzing find many of these bugs before they reach production. But "find bugs before production" is defense-in-depth, not elimination. The question Rust asks is: what if these bugs were type errors?


Rust ownership model — data flow showing ownership transfer and borrow checker guarantees

How Rust's Ownership Model Works

Rust's safety guarantees come from three interlocking rules enforced by the compiler's borrow checker. None of them require a garbage collector.

Rule 1: Ownership

Every value in Rust has exactly one owner. When the owner goes out of scope, the value is dropped (freed) automatically. No manual deallocation; no chance to forget.

fn process_request(data: Vec<u8>) {
    // `data` is owned by this function
    let result = parse_payload(&data);
    println!("Parsed {} bytes", result.len());
    // `data` is automatically dropped here — memory freed
}

There is no way to access data after this function returns. The compiler won't compile code that tries.

Rule 2: Borrowing

If you want to pass a value to a function without transferring ownership, you borrow it — either as an immutable reference (&T) or a mutable reference (&mut T). The borrow checker enforces two invariants:

  1. At any given time, you can have either one mutable reference or any number of immutable references — not both.
  2. References must never outlive the value they reference.

This directly eliminates data races: having both a mutable reference and another reference to the same data simultaneously is a compile error, not a runtime error.

fn analyze_headers(headers: &[u8]) -> SecurityResult {
    // `headers` is borrowed; the caller still owns it
    // We can read, but we cannot modify or free it
    parse_security_headers(headers)
}

fn main() {
    let raw = read_request_bytes();
    let result = analyze_headers(&raw); // borrow
    log_result(&result);                 // raw is still valid here
} // raw dropped here

Rule 3: Lifetimes

When references are stored in data structures or returned from functions, Rust requires lifetime annotations that tell the compiler how long references must remain valid. The compiler verifies that no reference outlives its underlying data.

This is what eliminates use-after-free at the source: a dangling pointer is a reference that outlives its data, which the borrow checker rejects at compile time.

// Lifetime annotation: the returned reference lives as long as `input`
fn extract_token<'a>(input: &'a str, prefix: &str) -> Option<&'a str> {
    input.strip_prefix(prefix)
}

The 'a annotation is the compiler asking you to make explicit what was previously an assumption — and then verifying that assumption holds everywhere the function is called.

What This Eliminates

  • Buffer overflow: Rust's standard library containers perform bounds checking on slice access. Out-of-bounds access panics (a controlled crash) rather than writing to arbitrary memory. In security-sensitive code, you can return None or Err instead.
  • Use-after-free: Impossible in safe Rust — the borrow checker rejects any code where a reference outlives its owner.
  • Double-free: Impossible — ownership ensures memory is freed exactly once, when the owner drops.
  • Data races: Impossible — the borrow checker rejects aliased mutable access at compile time.
  • Null pointer dereference: Rust has no null. The Option<T> type forces explicit handling of the absent case.

flowchart TD A[Value Created] --> B{Ownership Check} B -->|Single Owner| C[Owner Scope] C --> D{Borrow?} D -->|Immutable Borrow &T| E[Multiple readers OK] D -->|Mutable Borrow &mut T| F[Exclusive access] D -->|Move ownership| G[New Owner] E --> H{Lifetime valid?} F --> H G --> I[Old owner invalidated] H -->|Yes| J[Compile succeeds ✓] H -->|No — dangling ref| K[Compile error ✗] I --> J C --> L[Owner out of scope] L --> M[Memory freed automatically] style K fill:#ff6b6b,color:#fff style J fill:#51cf66,color:#fff style M fill:#339af0,color:#fff

Implementation Guide: Writing Secure Rust

Understanding the model is one thing. Using it in practice is another. Here are the patterns that matter most for security-sensitive code.

Handling Untrusted Input

All security-critical code starts with untrusted input. Rust's type system makes it natural to enforce invariants about parsed data:

use std::io::{self, Read};

#[derive(Debug)]
pub struct ParsedRequest {
    pub method: HttpMethod,
    pub path: ValidatedPath,
    pub headers: Vec<(String, String)>,
    pub body: Vec<u8>,
}

pub fn parse_request(raw: &[u8]) -> Result<ParsedRequest, ParseError> {
    let limit = 8 * 1024 * 1024; // 8MB hard limit
    if raw.len() > limit {
        return Err(ParseError::RequestTooLarge { size: raw.len(), limit });
    }

    let (header_section, body) = split_headers(raw)?;
    let request_line = parse_request_line(header_section)?;

    Ok(ParsedRequest {
        method: request_line.method,
        path: ValidatedPath::parse(&request_line.path)?,
        headers: parse_headers(header_section)?,
        body: body.to_vec(),
    })
}

The Result<T, E> return type forces the caller to handle the error case. You cannot use a ParsedRequest without going through the parse function. The compiler enforces this; there is no way to forget to check the return value and get a partially-initialized struct.

Running this against a corpus of malformed HTTP requests:

$ cargo test --test fuzz_parse_request -- --nocapture
running 50 fuzz cases...
  malformed_request_line: Ok(Err(InvalidRequestLine))
  missing_crlf: Ok(Err(MissingCrlf))
  oversized_header: Ok(Err(HeaderTooLarge { size: 16400, limit: 8192 }))
  embedded_nul: Ok(Err(InvalidBytes { offset: 47 }))
all 50 fuzz cases: no panics, no unsafe memory access

The unsafe Block: Your Security Perimeter

Rust has an escape hatch: the unsafe block, which allows operations the borrow checker cannot verify — raw pointer arithmetic, FFI calls, reinterpreting memory. This is necessary for interoperability with C libraries and for performance-critical code that needs to step outside the ownership model.

For security engineers, unsafe blocks are the audit surface. Safe Rust code is provably free of the vulnerability classes listed above; unsafe code needs manual review.

Best practice: contain unsafe behind a safe abstraction.

// The unsafe block is encapsulated; callers use a safe API
pub fn parse_fixed_header(buf: &[u8]) -> Option<FixedHeader> {
    if buf.len() < std::mem::size_of::<RawHeader>() {
        return None;
    }

    // SAFETY: we just verified `buf` is large enough, and `RawHeader` is
    // repr(C) with no padding. This is a valid alignment for this platform.
    let raw: &RawHeader = unsafe {
        &*(buf.as_ptr() as *const RawHeader)
    };

    FixedHeader::validate(raw)
}

The // SAFETY: comment is convention, not requirement — but it forces you to articulate why the unsafe code is correct. It's the equivalent of a CVE pre-mortem.

Cryptographic Code in Rust

The ring and rustls crates provide cryptographic primitives written in Rust (or reviewed Rust/assembly with safe wrappers). Both are widely used in production and have been audited.

use ring::{digest, hmac};

pub fn compute_request_hmac(
    key: &hmac::Key,
    method: &str,
    path: &str,
    timestamp: u64,
    body: &[u8],
) -> hmac::Tag {
    let mut ctx = hmac::Context::with_key(key);
    ctx.update(method.as_bytes());
    ctx.update(b"\n");
    ctx.update(path.as_bytes());
    ctx.update(b"\n");
    ctx.update(&timestamp.to_be_bytes());
    ctx.update(b"\n");
    ctx.update(body);
    ctx.sign()
}

Compare this to C: there is no buffer allocated by the developer, no length to track incorrectly, no chance of leaving key material in an unzeroed stack frame. The ring crate zeroes sensitive memory on drop.


flowchart TD A[Untrusted Input Arrives] --> B{Check with unsafe?} B -->|No — pure safe Rust| C[Borrow checker validates] B -->|Yes — FFI / raw ptrs| D[Encapsulate in safe wrapper] C --> E{Return Result/Option?} D --> F{SAFETY comment + audit?} F -->|No| G[Flag for manual review ⚠️] F -->|Yes| E E -->|Err/None path handled| H[Propagate or recover] E -->|All paths handled| I[Type-safe output] H --> I I --> J{Sensitive data?} J -->|Yes — use Zeroize trait| K[Memory zeroed on drop] J -->|No| L[Normal drop] style G fill:#ffa94d,color:#fff style K fill:#51cf66,color:#fff

C/C++ vs Rust — memory safety comparison: danger zone vs safe zone

Comparison and Tradeoffs

No language is universally correct. Here's an honest view of where Rust sits relative to alternatives.

Language Memory Safety Performance CVE Class Eliminated Learning Curve Ecosystem Maturity
C None (manual) Baseline None Low 50+ years
C++ (modern) Partial (smart ptrs) ~C Reduced (not eliminated) High Mature
Go GC-based ~20% slower Most (GC handles lifetime) Low-Medium Growing fast
Rust Compile-time ~C All (in safe code) High Growing fast
Java/JVM GC-based 2-5× slower Most Medium Mature
Swift ARC + safety Close to C Most Medium Apple ecosystem

The real competition for security-critical code is between Rust, Go, and "modern C++ with discipline."

Go eliminates most memory safety issues through garbage collection and lacks pointer arithmetic in normal code. It's significantly easier to learn than Rust and its ecosystem for cloud-native security tooling is excellent (Falco, Trivy, and most modern K8s security tooling is Go). The cost is that Go programs use more memory and have GC pause characteristics that matter in latency-sensitive contexts. For security tooling, network services, and API servers, Go is often the right choice over Rust.

Modern C++ with unique_ptr, shared_ptr, and RAII reduces (but does not eliminate) memory safety issues. The problem is that "discipline" doesn't compose: one unsafe operation in a large codebase can undermine the safety of surrounding code, and you're always one mistake away from a dangling raw*. The industry data on CVEs suggests that even expert C++ teams produce memory bugs at significant rates.

Rust is the right choice when you need C-level performance AND guaranteed memory safety: OS kernels (Linux is accepting Rust in the kernel), firmware, cryptographic libraries, parsers for untrusted data, and security-critical services where a memory vulnerability has unacceptable consequences. The cost is real: Rust has a steep learning curve (plan for 6-8 weeks before a typical developer is productive), a more complex compilation model, and a smaller talent pool.


timeline title Memory Safety in Systems Languages — Evolution 1972 : C released — explicit malloc/free : Developer owns all memory management 1985 : C++ — RAII introduced (constructors/destructors) : Reduces leaks but doesn't eliminate use-after-free 1995 : Java/JVM — garbage collection mainstream : Memory safety via GC; performance cost 2007 : Go released — GC with simpler memory model : Strong safety for web/cloud; GC pauses remain 2010 : Rust development begins at Mozilla : Borrow checker concept takes shape 2015 : Rust 1.0 — ownership model stabilized : First production-grade memory-safe systems language 2019 : Microsoft discloses 70% CVE figure : Public acknowledgement of the C/C++ problem at scale 2022 : NSA recommends memory-safe languages : Linux kernel begins accepting Rust contributions 2024 : White House ONCD report on memory safety : Android team: same 70% ratio in their CVE data 2026 : Rust in Linux kernel stable (drivers, networking) : CISA memory safety roadmap guidance published

Production Considerations

Cargo Audit: Dependency Vulnerability Scanning

Rust's package manager Cargo makes it easy to add dependencies. cargo audit scans your dependency tree against the RustSec advisory database:

$ cargo audit
    Fetching advisory database from `https://github.com/RustSec/advisory-db.git`
      Loaded 639 security advisories (from /home/.cargo/advisory-db)
    Scanning Cargo.lock for vulnerabilities (424 crate dependencies)
    Crate:         openssl
    Version:       0.10.45
    Warning:       unmaintained
    Title:         openssl is unmaintained; prefer rustls
    Date:          2023-11-28
    ID:             RUSTSEC-2023-0072
    URL:            https://rustsec.org/advisories/RUSTSEC-2023-0072

Run this in CI. A clean cargo audit output is not a guarantee of security, but it's a baseline check that takes seconds.

Integrating with Existing C/C++ Code: FFI

Most real systems aren't greenfield Rust. The common migration pattern is:

  1. Write new security-critical components in Rust (parsers, crypto, auth logic)
  2. Expose a C-compatible interface with #[no_mangle] and extern "C" declarations
  3. Wrap unsafe FFI calls in safe Rust abstractions
  4. Gradually expand the Rust footprint

The bindgen crate auto-generates Rust FFI bindings from C headers. cbindgen generates C headers from Rust. Between them, Rust-C interop is tractable.

// Safe wrapper around an unsafe FFI call to a C crypto library
pub fn legacy_decrypt(
    key: &[u8; 32],
    iv: &[u8; 16],
    ciphertext: &[u8],
) -> Result<Vec<u8>, CryptoError> {
    if ciphertext.is_empty() {
        return Err(CryptoError::EmptyCiphertext);
    }
    let mut output = vec![0u8; ciphertext.len()];
    let result = unsafe {
        // SAFETY: all slices are non-null, correctly sized.
        // `output` has capacity for the plaintext.
        sys::legacy_aes_decrypt(
            key.as_ptr(),
            iv.as_ptr(),
            ciphertext.as_ptr(),
            ciphertext.len(),
            output.as_mut_ptr(),
        )
    };
    if result == 0 {
        Ok(output)
    } else {
        Err(CryptoError::DecryptionFailed { code: result })
    }
}

Fuzz Testing with cargo-fuzz

Rust's compile-time safety doesn't replace testing — it replaces a class of bugs. Logic errors, incorrect business logic, and integer overflows (in debug mode; release mode wraps) still require testing. Fuzz testing is particularly valuable for parsers:

cargo install cargo-fuzz
cargo fuzz init
cargo fuzz add fuzz_parse_request
cargo fuzz run fuzz_parse_request -- -max_total_time=3600

AddressSanitizer and UBSanitizer are built into the fuzzer harness. Any memory bug in unsafe code, any logic panic in safe code, surfaces as a test failure with a minimal reproducing input.

The #[deny(unsafe_code)] Pragma

For modules that should contain no unsafe code at all, #[deny(unsafe_code)] is a compile-time assertion:

#![deny(unsafe_code)]

// This module is guaranteed to contain no unsafe operations.
// Any PR that adds an unsafe block will fail to compile.
pub mod auth;
pub mod token_validation;
pub mod input_sanitization;

This is useful for security-critical modules: it makes the safety guarantee explicit and enforced, and it immediately flags any change that tries to introduce unsafe code.


Conclusion

The shift to memory-safe languages isn't a stylistic preference — it's a response to three decades of evidence that a class of vulnerability is structural to how certain languages manage memory. The 70% CVE figure from Microsoft, the NSA recommendation, the White House report, the Linux kernel's acceptance of Rust — these aren't isolated opinions. They're the industry reaching a conclusion based on accumulated data.

Rust doesn't eliminate all security vulnerabilities. Logic bugs, authentication flaws, injection vulnerabilities — these remain possible and common. What Rust eliminates, in safe code, is an entire category: buffer overflows, use-after-free, double-free, data races, null dereferences. These vulnerabilities are exploited constantly. Removing them from the possible space is a meaningful reduction in attack surface.

For security engineers, the practical message is:

  1. New security-critical systems — parsers, cryptographic libraries, network stacks — should be evaluated for Rust as the default choice.
  2. Existing C/C++ systems — use cargo-fuzz, ASAN, and cargo audit as an improvement layer. Migrate incrementally where the risk profile justifies it.
  3. Audit surface — in any Rust codebase, unsafe blocks are the review priority. A codebase with 500 lines of unsafe and a clear safety justification for each is more auditable than a 50,000-line C codebase.
  4. Toolingcargo audit, clippy, and rustfmt are table stakes. Add them to CI before the first merge.

The bug I spent three days hunting in 2019 couldn't exist in safe Rust. That's the argument in one sentence.


Sources

  1. Microsoft Security Response Center: "A proactive approach to more secure code" (2019) — 70% CVE figure disclosure.
  2. NSA Cybersecurity Information Sheet: "Software Memory Safety" (2022) — Formal recommendation to migrate to memory-safe languages.
  3. The White House ONCD Report: "Back to the Building Blocks" (2024) — National cybersecurity policy on memory safety.
  4. RustSec Advisory Database — Vulnerability database for Rust crates.
  5. Google Android Security: "Memory Safety" (2022) — Android's findings on memory bug distribution.
  6. Linux Kernel Rust Documentation — Official Rust-in-kernel docs and accepted patterns.

About the Author

Toc Am

Founder of AmtocSoft. Writing practical deep-dives on AI engineering, cloud architecture, and developer tooling. Previously built backend systems at scale. Reviews every post published under this byline.

LinkedIn X / Twitter

Published: 2026-04-22 · Written with AI assistance, reviewed by Toc Am.

Get These In Your Inbox

Weekly deep-dives on AI engineering, no fluff. Join the newsletter →

Subscribe (free)

Or grab the book ($39, ~100 pages) · Buy me a coffee

Buy Me a Coffee · 🔔 YouTube · 💼 LinkedIn · 🐦 X/Twitter

Sunday, April 12, 2026

Building a Security Culture: DevSecOps, Shifting Left, and the Human Layer Attackers Target

Hero image: developer with security tools integrated into their workflow

Generated with Higgsfield GPT Image — 16:9

Introduction

The 2025 Verizon Data Breach Investigations Report landed with a statistic that should unsettle every engineering team: 98% of breaches involve a human element. Not a misconfigured firewall. Not an unpatched library. A human — someone who clicked the wrong link, approved the wrong request, or typed a secret into the wrong field.

That number has held steady for years. And yet, the typical organizational response to security incidents is to add another tool to the CI/CD pipeline.

Tools matter. But they are not the bottleneck. The bottleneck is culture.

In 2026, most engineering organizations have nominally adopted "DevSecOps." They have Dependabot configured. They run container scans in GitHub Actions. They have a SAST tool that fails the build when it detects certain patterns. And they still get breached — because the culture around security hasn't changed. Developers still think of security as something AppSec does after the PR is merged. Security teams still think of developers as the source of problems rather than the first line of defense. And everyone treats the annual security training as a mandatory checkbox before they can get back to shipping.

This post is about closing that gap. We will cover what DevSecOps actually means when implemented with cultural intent rather than as a tool procurement exercise, what "shifting left" looks like in a real developer workflow with concrete tooling configurations, and — critically — how attackers actually bypass all of your technical controls by targeting the humans in your organization.

By the end, you should have a concrete starting point for building a security culture that makes your technical controls more effective, not less. This is the fourth post in our AI Security series; you may also want to read AI-Powered Cybersecurity (061), Deepfake Phishing and AI Attacks (062), and AI Compliance for Developers (063).


What DevSecOps Actually Means

DevSecOps was coined to make a simple argument: security belongs in the development and operations lifecycle, not outside it. The original premise was sound. The implementation has mostly been terrible.

The buzzword version of DevSecOps looks like this: a security team attends a DevOps conference, comes back with a list of tools, integrates them into the CI/CD pipeline, and declares victory. Developers are now blocked from merging unless their code passes a suite of security gates they did not design, do not understand, and cannot fix without asking AppSec. AppSec, overwhelmed by false positives, either turns down the sensitivity or becomes a perpetual review bottleneck. The result is friction without security.

The actual version of DevSecOps looks different in three critical ways.

Security is a shared responsibility, not a gate. In mature organizations, security is not something that happens to developers at the end of a pull request. It is something developers participate in actively, from the design phase onward. This means security requirements appear in tickets before a line of code is written. It means threat modeling is a design exercise, not an afterthought. It means developers know how to read a SAST finding, distinguish a true positive from a false positive, and remediate the underlying issue — not just suppress the warning.

Developers are the first line of defense. This is not rhetorical. The developer is the only person in the organization who touches the code before it ships. By the time AppSec reviews it, the cost of fixing a vulnerability has already multiplied. IBM's Systems Sciences Institute put the ratio at roughly 100:1 between finding a defect in production versus in design. Treating developers as security agents — rather than security liabilities — is the highest-leverage intervention available to any security program.

Blameless postmortems apply to security incidents. Engineering organizations that have embraced Site Reliability Engineering principles understand blameless postmortems: when something goes wrong, the goal is to understand why the system allowed it to happen, not to assign fault to an individual. This principle applies with equal force to security incidents. When a developer accidentally commits a secret to a public repository, the question is not "why did this developer do something irresponsible?" The question is: "Why does our system allow secrets to be committed? Why did no pre-commit hook catch it? Why did no CI gate flag it?" Blame drives security failures underground. Blameless culture surfaces them so they can be fixed.

Security champions programs. A security champions program designates one developer per team — typically a mid-to-senior engineer with interest in security — as a liaison between the development team and the AppSec function. Champions attend security briefings, participate in threat modeling, and serve as the first point of contact when teammates have security questions. They are not responsible for making all security decisions; they are responsible for making security accessible and removing the friction of "I need to file a ticket with AppSec to ask a basic question." Research from SANS consistently shows that organizations with active security champions programs detect vulnerabilities earlier and resolve them faster.

Architecture diagram: DevSecOps pipeline — plan, code, build, test, release, deploy, operate, monitor

Generated with Higgsfield GPT Image — 16:9

The following diagram shows how security integrates across each phase of the software development lifecycle, rather than sitting as a gate at the end:

graph TD A[Plan] -->|Threat modeling, security requirements| B[Code] B -->|Pre-commit hooks, IDE plugins, SAST| C[Build] C -->|Dependency scanning, secrets detection, SAST gates| D[Test] D -->|DAST, container scanning, IaC scanning| E[Release] E -->|Security sign-off, pentest, compliance check| F[Deploy] F -->|Secrets management, policy enforcement, SBOM| G[Operate] G -->|SIEM, anomaly detection, threat intelligence| H[Monitor] H -->|Incident response, blameless postmortem| A style A fill:#4A90D9,color:#fff style B fill:#5BA65B,color:#fff style C fill:#5BA65B,color:#fff style D fill:#5BA65B,color:#fff style E fill:#E8A838,color:#fff style F fill:#E8A838,color:#fff style G fill:#D9534F,color:#fff style H fill:#D9534F,color:#fff

The cycle is continuous, not linear. Every monitoring finding feeds back into the planning phase, tightening controls in response to what attackers are actually attempting.


Shifting Left: Security in the Developer Workflow

"Shift left" means moving security activities earlier in the development process — toward the left side of the timeline where changes are cheap rather than toward the right side where they are expensive. In practice, it means four concrete things: pre-commit hooks, CI/CD gates, IDE integration, and threat modeling in design.

Pre-Commit Hooks: Your First Automated Gate

Pre-commit hooks run before a commit is finalized on the developer's machine. They are the fastest possible feedback loop — the developer learns about a problem before it ever reaches a remote repository. The two most important categories to enforce at commit time are secrets detection and lightweight static analysis.

Secrets detection prevents API keys, tokens, database passwords, and private keys from ever entering version control. Two leading tools are git-secrets (AWS) and detect-secrets (Yelp). The following .pre-commit-config.yaml configures both alongside semgrep for lightweight SAST:

# .pre-commit-config.yaml
repos:
  # Secrets detection with detect-secrets
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.4.0
    hooks:
      - id: detect-secrets
        args:
          - '--baseline'
          - '.secrets.baseline'
        exclude: |
          (?x)^(
            .*\.lock$|
            .*package-lock\.json$|
            .*\.min\.js$
          )$

  # General pre-commit checks
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: check-merge-conflict
      - id: detect-private-key
      - id: check-added-large-files
        args: ['--maxkb=1000']
      - id: end-of-file-fixer
      - id: trailing-whitespace

  # Semgrep SAST — runs a targeted ruleset locally
  - repo: https://github.com/returntocorp/semgrep
    rev: v1.56.0
    hooks:
      - id: semgrep
        args:
          - '--config=auto'
          - '--error'
          - '--severity=ERROR'
          # Skip low-signal rules to reduce noise
          - '--exclude-rule=generic.secrets.security.detected-generic-secret.detected-generic-secret'
        pass_filenames: false

Initialize the detect-secrets baseline on a new repository with:

# Create initial baseline (scan existing codebase, mark known non-secrets)
detect-secrets scan > .secrets.baseline

# Install hooks for the project
pre-commit install

# Run against all existing files (one-time audit)
pre-commit run --all-files

The baseline file is committed to the repository. When new developers clone the repo, pre-commit install wires up the hooks on their machine. The secrets baseline records which patterns in existing files are known-safe, so the hook does not flag things like example credentials in documentation.

CI/CD Gates: Defense in Depth

Pre-commit hooks can be bypassed with --no-verify. CI/CD gates cannot. The following GitHub Actions workflow runs dependency scanning with Snyk, container image scanning with Trivy, and a full semgrep scan on every pull request:

# .github/workflows/security-scan.yml
name: Security Scan

on:
  pull_request:
    branches: [main, develop]
  push:
    branches: [main]

permissions:
  contents: read
  security-events: write  # Required for SARIF upload to GitHub Security tab
  pull-requests: write    # Required for PR comments

jobs:
  dependency-scan:
    name: Dependency Vulnerability Scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Snyk to check for vulnerabilities
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: >
            --severity-threshold=high
            --sarif-file-output=snyk.sarif
        continue-on-error: true  # Don't block on first run — triage first

      - name: Upload Snyk SARIF to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: snyk.sarif
        if: always()

  container-scan:
    name: Container Image Scan (Trivy)
    runs-on: ubuntu-latest
    needs: dependency-scan
    steps:
      - uses: actions/checkout@v4

      - name: Build Docker image for scanning
        run: docker build -t app:${{ github.sha }} .

      - name: Run Trivy container scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: app:${{ github.sha }}
          format: sarif
          output: trivy-results.sarif
          severity: 'CRITICAL,HIGH'
          exit-code: '1'       # Fail on CRITICAL/HIGH findings
          ignore-unfixed: true # Skip vulns with no available fix

      - name: Upload Trivy SARIF to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy-results.sarif
        if: always()

  sast-scan:
    name: Static Analysis (Semgrep)
    runs-on: ubuntu-latest
    container:
      image: semgrep/semgrep
    steps:
      - uses: actions/checkout@v4

      - name: Run Semgrep full ruleset
        run: |
          semgrep \
            --config=p/owasp-top-ten \
            --config=p/secrets \
            --config=p/supply-chain \
            --sarif \
            --output=semgrep.sarif \
            --severity=WARNING
        env:
          SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}

      - name: Upload Semgrep SARIF
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: semgrep.sarif
        if: always()

      - name: Comment findings on PR
        uses: github/codeql-action/analyze@v3
        if: github.event_name == 'pull_request'

The SARIF upload integration means findings appear natively in the GitHub Security tab and as inline PR comments. Developers see security findings in the same interface where they review code — reducing the friction of "I need to go to a separate dashboard to understand what this scan found."

IDE Plugins: Real-Time Feedback

The fastest feedback loop of all is finding a vulnerability as it is typed. Two plugins are worth standardizing across your engineering organization:

  • Snyk Security (VS Code, JetBrains): Scans dependencies and code in real time. Highlights vulnerable imports with inline severity indicators and links to remediation guidance.
  • Semgrep (VS Code): Runs configured rulesets as you type. Particularly useful for organization-specific rules — you can write a Semgrep rule that catches patterns unique to your codebase (e.g., "never call this deprecated internal auth function directly").

Threat Modeling in Design

The highest-leverage shift-left practice does not involve any tool. It involves adding a structured security conversation to the design phase of every significant feature.

STRIDE is the most widely used threat modeling framework for application security. For each component in a system design, it asks whether the component is exposed to: Spoofing (impersonation), Tampering (data modification), Repudiation (denying actions), Information Disclosure, Denial of Service, or Elevation of Privilege. A 30-minute STRIDE exercise on a design doc will surface more actionable security findings than a week of post-hoc scanning, because it catches architectural problems that no scanner can detect.

The following diagram shows how security gates map to each SDLC phase, moving from design-time threat modeling through to production monitoring:

graph LR subgraph Design ["Design Phase"] TM[Threat Modeling
STRIDE / PASTA] SR[Security Requirements
in Tickets] end subgraph Code ["Code Phase"] PC[Pre-Commit Hooks
detect-secrets, semgrep] IDE[IDE Plugins
Snyk, Semgrep] end subgraph Build ["Build / CI Phase"] SAST[SAST Gate
Semgrep + OWASP rules] SCA[SCA Gate
Snyk / Dependabot] end subgraph Test ["Test Phase"] CT[Container Scan
Trivy] DAST[DAST
OWASP ZAP] IaC[IaC Scan
Checkov / tfsec] end subgraph Release ["Release / Deploy"] SEC[Security Sign-off
Champion Review] SM[Secrets Mgmt
Vault / AWS SM] end subgraph Operate ["Operate / Monitor"] SIEM[SIEM Alerts
CloudTrail / Splunk] WAF[WAF + Bot Mgmt] end TM --> SR --> PC --> IDE --> SAST --> SCA --> CT --> DAST --> IaC --> SEC --> SM --> SIEM --> WAF style Design fill:#E8F4FD,stroke:#4A90D9 style Code fill:#EDF7ED,stroke:#5BA65B style Build fill:#EDF7ED,stroke:#5BA65B style Test fill:#FFF8E7,stroke:#E8A838 style Release fill:#FFF8E7,stroke:#E8A838 style Operate fill:#FDECEA,stroke:#D9534F

The key insight is that each gate catches a different class of problem, and the classes compound: threat modeling catches architectural flaws, pre-commit catches secrets, SAST catches code patterns, SCA catches dependency CVEs, container scanning catches OS-level vulnerabilities, and DAST catches runtime issues. No single gate is sufficient, and no combination of gates replaces a security-aware engineering culture.


The Human Layer: Social Engineering in 2026

Here is the uncomfortable reality that a perfectly configured DevSecOps pipeline cannot address: the most reliable attack vector against a well-defended organization is the person with legitimate access.

Attackers know this. The economics are brutal. Exploiting a zero-day vulnerability requires significant technical expertise, costs tens of thousands of dollars on the underground market, and works for a limited window before it is patched. Sending a convincing phishing email costs cents, requires no technical expertise, and works against organizations regardless of how mature their security tooling is.

The attack surface is not your code. It is your people.

AI-Personalized Phishing

In 2024 and 2025, we documented the emergence of AI-personalized phishing at scale — a development covered in depth in post 062 of this series. The key shift: phishing campaigns historically relied on mass distribution of identical lures, making detection via pattern matching tractable. AI-generated phishing produces individually personalized messages drawn from public LinkedIn profiles, GitHub contributions, company blog posts, and OSINT databases.

A developer who recently merged a PR to the payments service receives an email, apparently from a trusted vendor, referencing that specific work and asking them to review a new API authentication specification. The tone matches the vendor's communication style. The link leads to a convincing credential harvest page. No technical control in your CI/CD pipeline stops this.

Vishing and Smishing

Voice phishing (vishing) has seen a significant resurgence, partly driven by AI voice cloning. An attacker who has harvested a target's contact information — readily available via data broker databases — can call a help desk impersonating an employee and request a password reset, citing urgency. Smishing (SMS phishing) follows the same pattern with a different channel. Multi-factor authentication mitigates some of this, but cannot stop an attacker who social engineers a help desk into bypassing MFA procedures.

Insider Threats

The insider threat is not primarily malicious employees. It is well-intentioned employees making poor decisions under pressure — sharing credentials with a colleague to "speed things up," forwarding a sensitive document to a personal email to work from home, installing an unapproved tool to solve an immediate problem. These behaviors are rational from the individual's perspective. They become security incidents when the credential is compromised, the personal email account is breached, or the unapproved tool exfiltrates data.

Vendor and Supply Chain Social Engineering

The SolarWinds attack, the 3CX supply chain attack, and numerous subsequent incidents have made clear that attackers increasingly target vendors and service providers as an indirect route into well-defended targets. Social engineering a developer at a smaller vendor with weaker security controls is often easier than attacking the target directly. From a culture perspective, this means your supply chain security posture is only as good as your third-party risk management and the security culture you require from vendors.

The trust exploitation model underlying all of these attacks is the same: attackers do not break in, they log in — using credentials, session tokens, or social authority borrowed from legitimate users. Your technical controls assume attackers are outsiders presenting invalid credentials. The human layer determines how often legitimate-seeming requests from actual attackers pass through.

Comparison: Reactive security culture vs. proactive security culture

Generated with Higgsfield GPT Image — 16:9


Building the Human Layer of Defense

If the human layer is the primary attack surface, then the primary defensive investment should be in developing the human layer. This is not a novel insight — the information security community has said it for decades. The consistent failure to act on it is organizational, not intellectual: security awareness is treated as a cost center, measured by completion rates rather than behavioral change, and starved of investment relative to tool procurement.

Effective human-layer defense programs share five characteristics.

Continuous Simulation Over Annual Training

Annual security awareness training has the weakest evidence base of any security investment. Employees sit through a 45-minute video, pass a multiple-choice quiz, and forget the content within 60 days. The checkbox is checked, the compliance requirement is satisfied, and the phishing click rate remains unchanged.

What works is continuous, simulated training with immediate feedback. Phishing simulation platforms (KnowBe4, Proofpoint Security Awareness, Cofense) send realistic phishing lures to employees and immediately redirect anyone who clicks to a brief training module explaining what they fell for. The critical design principle is immediacy: the training occurs at the moment of failure, when the cognitive connection between the action and the risk is strongest.

Organizations running quarterly phishing simulations typically see click rates decline from an industry-average 15-20% at baseline to under 5% within a year. The measurement is the mechanism: when employees know simulations happen, and that clicking results in a training experience rather than a reprimand, they develop a reflex of scrutiny rather than a reflex of compliance.

Security Champions: Proximity to the Team

A central AppSec team cannot scale to the needs of a large engineering organization. A security team of five cannot provide meaningful security guidance to fifty development teams. Security champions solve this through distribution: by training one security-minded developer per team and empowering them with the knowledge and authority to raise security concerns, you create a security presence in every standup, every design review, and every PR cycle.

Effective security champions programs include:
- Dedicated training time: champions attend AppSec conferences, take certifications, and participate in internal security guilds.
- Clear scope: champions are security advocates and escalation paths, not security gatekeepers. They should not be blamed when their team ships a vulnerability — their role is to raise the floor, not to be personally responsible for every decision.
- Cross-team visibility: a monthly security champions meeting creates a network where common issues can be discussed and addressed systematically rather than solved redundantly by each team in isolation.
- Recognition: champions receive visible credit in performance reviews and public acknowledgment. Security work that is invisible becomes a career penalty; making it visible makes it a career accelerator.

Blameless Reporting Culture

The single most effective way to suppress security intelligence in an organization is to punish reporters. If a developer who accidentally commits a secret to a public repository is publicly shamed or faces formal discipline, every other developer in the organization learns to hide their mistakes. Security incidents go unreported. Near-misses disappear. The organization loses its best source of information about where controls are failing.

Blameless reporting means: when someone reports a security incident — whether they caused it or discovered it — the response is "thank you for telling us, let's fix it and understand how to prevent it" rather than "who is responsible for this?" This requires active modeling from leadership. The first time a VP handles a security incident blamefully, the message is set for the entire organization regardless of what the policy document says.

Practical mechanisms:
- Anonymous reporting channels (separate from standard incident tickets) for reporting concerns without fear of identification.
- Explicit "bug bounty" framing for internal reports: celebrate the person who found the credential in the log file before an attacker did.
- Post-incident communications that focus on systemic causes, not individual failures.

Clear Escalation Paths

One reason security incidents go unreported is that employees do not know who to tell or fear that reporting will expose them to undefined consequences. A clear, publicized escalation path removes both barriers.

The path should be: employee → security champion → AppSec team → CISO or security leadership, with defined response time SLAs at each level and explicit protection for reporters. Every employee should be able to answer "who do I tell if I think something is wrong?" without having to think about it.

Tabletop Exercises

Tabletop exercises simulate security incidents in a discussion format. The security team presents a scenario ("we have received an alert that a developer's credentials are being used from two geographically distant locations simultaneously — walk us through your response"), and the relevant teams work through their response in real time.

Tabletops surface gaps that documentation reviews miss: the escalation path that worked in theory turns out to require a person who is on vacation; the incident response runbook assumes a tool that has been decommissioned; the communication plan has no provision for an incident that occurs outside business hours. These gaps are cheap to find in a tabletop and expensive to discover during an actual incident.

The following diagram shows the security champion network and how information and authority flow between the organization's security layers:

graph TD CISO["CISO / Security Leadership
Strategy, Risk Decisions, Escalations"] AppSec["AppSec Team
Tooling, Standards, Incident Response, Training"] subgraph eng1 ["Team 1 — Platform"] C1["Security Champion
(Senior Dev)"] D1A["Developer"] D1B["Developer"] D1C["Developer"] end subgraph eng2 ["Team 2 — Payments"] C2["Security Champion
(Senior Dev)"] D2A["Developer"] D2B["Developer"] end subgraph eng3 ["Team 3 — Mobile"] C3["Security Champion
(Senior Dev)"] D3A["Developer"] D3B["Developer"] D3C["Developer"] end subgraph eng4 ["Team 4 — Data"] C4["Security Champion
(Mid Dev)"] D4A["Developer"] D4B["Developer"] end CISO <--> AppSec AppSec <--> C1 AppSec <--> C2 AppSec <--> C3 AppSec <--> C4 C1 <--> D1A C1 <--> D1B C1 <--> D1C C2 <--> D2A C2 <--> D2B C3 <--> D3A C3 <--> D3B C3 <--> D3C C4 <--> D4A C4 <--> D4B style CISO fill:#D9534F,color:#fff style AppSec fill:#E8A838,color:#fff style C1 fill:#5BA65B,color:#fff style C2 fill:#5BA65B,color:#fff style C3 fill:#5BA65B,color:#fff style C4 fill:#5BA65B,color:#fff

The champion layer is the critical translation layer. Without it, AppSec communicates to developers primarily through automated blocking mechanisms — scan failures, policy gates, rejected PRs — which creates adversarial dynamics. With it, security guidance travels through trusted colleagues who speak the same technical language and share the same delivery pressures.


Measuring Security Culture

Security culture is not a soft concept. It is measurable, and measuring it is essential to improving it. Without measurement, security programs drift toward activity metrics (how many trainings delivered, how many tools deployed) that say nothing about whether the culture is actually more secure.

The metrics that matter fall into two categories: lagging indicators that tell you how your controls performed, and leading indicators that tell you how your culture is evolving.

Lagging Indicators

Mean Time to Detect (MTTD): The average time between when a security incident begins and when it is detected by your organization. Industry median for breach detection in 2025 was 197 days (IBM Cost of a Data Breach Report). Every day in that number is an attacker with undetected access. A declining MTTD is a signal that your monitoring and reporting culture is improving.

Mean Time to Respond (MTTR): From detection to containment. This measures the operational efficiency of your incident response, and reflects whether your escalation paths, runbooks, and team training are working.

Vulnerability Fix SLA Compliance: Given defined SLAs for remediating vulnerabilities by severity (e.g., critical findings fixed within 72 hours, high within 14 days), what percentage of findings are resolved within SLA? Low compliance indicates either tooling problems (too many false positives drowning real findings) or cultural problems (security remediation is deprioritized relative to feature work).

Security Debt Ratio: The ratio of open vulnerability findings to total code surface — a rough measure of whether security debt is accumulating faster than it is being repaid. An increasing ratio over time indicates the security program is losing ground even if absolute finding counts appear stable.

Leading Indicators

Phishing Click Rate Over Time: The most direct measure of security awareness program effectiveness. Track cohort-by-cohort across simulation campaigns, broken down by team and role. A declining click rate indicates the training is working. A flat or increasing rate indicates it is not.

Percentage of Teams with Active Security Champions: Coverage is a prerequisite to culture. If 60% of teams have champions and 40% do not, the 40% are operating without a security proximity layer — and incidents from those teams will reflect it.

Near-Miss Report Rate: The number of security near-misses voluntarily reported per quarter. An increasing rate is a positive signal: it means employees trust the reporting culture and believe reporting is worthwhile. A rate near zero indicates the blameless culture is not working or is not trusted.

Threat Model Coverage: What percentage of new features shipped in the last quarter included a formal threat modeling exercise? This is a measure of whether shift-left is actually happening or whether "threat modeling is a design phase activity" exists only in documentation.

OKRs for Security Culture

Security metrics translate naturally into OKRs. An example for a quarter:

Objective: Embed security into every team's development workflow.
- KR1: Pre-commit hooks deployed to 100% of repositories (currently 65%)
- KR2: Security champion coverage reaches 90% of engineering teams (currently 70%)
- KR3: Phishing click rate reduced from 12% to under 6% across Q2 simulations
- KR4: Mean time to remediate critical findings reduced from 8 days to 48 hours

OKRs at this level connect security culture investments directly to measurable outcomes, making them legible to engineering and product leadership in the same language they use for feature delivery. Security stops being "the department that blocks things" and becomes "the program that has committed to these outcomes and is tracking them publicly."


Conclusion

Security culture is a multiplier. Every technical control you deploy — pre-commit hooks, SAST gates, container scanning, MFA, WAF rules — performs better when the humans operating the system understand why those controls exist, have the skills to engage with their findings meaningfully, and feel empowered to raise concerns without fear.

The inverse is also true. No technical control is sufficient in an organization where developers suppress findings because raising them creates friction, where security is seen as an external team's responsibility rather than a shared one, and where the annual security training is something to be completed as quickly as possible before the real work resumes.

The shift that makes DevSecOps work is not buying a different tool. It is treating security as part of engineering practice rather than as a compliance overlay on top of it. That shift is harder than tool procurement. It requires leadership commitment, sustained investment in developer education, and the organizational patience to build culture over quarters rather than deploying it in a sprint.

The architecture is straightforward: automate the controls that can be automated, instrument the metrics that tell you how the culture is performing, build the champion network that distributes security knowledge across teams, and create the psychological safety that makes reporting possible. These investments compound. An organization that consistently executes them is dramatically harder to breach than one with superior tools and no culture to operate them effectively.

This post is part of our AI Security series. For the technical threat landscape, see AI-Powered Cybersecurity (061). For the social engineering attack vectors that bypass technical controls, see Deepfake Phishing and AI Attacks (062). For the regulatory environment your security program needs to satisfy, see AI Compliance for Developers (063).


Published by AmtocSoft Tech Insights — amtocsoft.blogspot.com

About the Author

Toc Am

Founder of AmtocSoft. Writing practical deep-dives on AI engineering, cloud architecture, and developer tooling. Previously built backend systems at scale. Reviews every post published under this byline.

LinkedIn X / Twitter

Published: 2026-04-12 · Written with AI assistance, reviewed by Toc Am.

Get These In Your Inbox

Weekly deep-dives on AI engineering, no fluff. Join the newsletter →

Subscribe (free)

Or grab the book ($39, ~100 pages) · Buy me a coffee

Buy Me a Coffee · 🔔 YouTube · 💼 LinkedIn · 🐦 X/Twitter

Bigger Is Not the Same as Better. The Job That Moved Is the Phone, Not the Lab.

Bigger is a plan. The phone is the receipt. The brief for this cycle is a question: does bigger always mean better in AI? The 2026 answer i...