This error happens before the linker and before TLS code runs: the compiler cannot locate the header named by #include <openssl/ssl.h>. On a normal native Ubuntu build, libssl-dev is the right package. If it is already installed, the interesting causes are usually a cross-compilation sysroot, a custom OpenSSL prefix, stale CMake cache, or build flags that discard the system include path.

Understand the package split

  • openssl provides command-line utilities.

  • The runtime package provides versioned shared libraries needed by already-built programs.

  • libssl-dev provides C headers, unversioned linker files, static archives where packaged, CMake configuration, and pkg-config metadata.

  • libssl implements TLS/DTLS and depends on libcrypto; crypto-only programs may need only libcrypto.

  • Installing development files fixes discovery, not source compatibility with a different OpenSSL major version.

1. Confirm the exact compiler failure

openssl_probe.cc
#include <openssl/opensslv.h>
#include <openssl/ssl.h>
#include <stdio.h>
 
int main(void)
{
    printf("%s\n", OpenSSL_version(OPENSSL_VERSION));
    return 0;
}

The probe needs both headers and libcrypto symbols

  • Angle-bracket includes ask the compiler’s configured include search paths.

  • opensslv.h exposes compile-time version declarations/macros.

  • ssl.h declares the TLS API.

  • OpenSSL_version is provided by libcrypto, so successful preprocessing alone is not the final test.

  • This probe reports the linked library version at runtime; it does not create a secure TLS connection.

Directory containing openssl_probe.cbash
cc -std=c17 -Wall -Wextra -Wpedantic -c openssl_probe.c
Before development headers are available, compilation stops with fatal error: openssl/ssl.h: No such file or directory (or at opensslv.h first).
  • -c stops after producing an object file and does not link libraries.

  • A missing header is a preprocessing/compilation search-path problem.

  • An “undefined reference” later is a linker dependency/order problem.

  • A loader error such as missing libssl.so is a runtime library search/ABI problem.

  • Diagnose the first failing stage instead of reinstalling packages indiscriminately.

2. Inspect package state before installing

Ubuntu shellbash
apt-cache policy libssl-dev
dpkg-query -W -f="${Status} ${Version}\n" libssl-dev 2>/dev/null || true
APT shows the repository candidate; dpkg reports install status/version when present.

Repository evidence prevents guesswork

  • apt-cache policy is read-only and reveals the selected repository version.

  • dpkg-query checks the local package database.

  • The escaped format prints status and version; || true is acceptable for this optional diagnostic, not for hiding build failures.

  • Ubuntu releases and enabled security/updates pockets supply different patched versions.

  • Do not add random PPAs merely to obtain a header that the supported repository already provides.

3. Install the development package

Ubuntu development hostbash
sudo apt update
sudo apt install libssl-dev pkg-config
APT refreshes package indexes, then asks to install/upgrade the development files and pkg-config tooling with repository-matched dependencies.

Risk level: caution. Review the command before running it.

Review the transaction before approving

  • Administrative commands change system packages.

  • libssl-dev must match the repository runtime ABI package selected by APT.

  • pkg-config provides build flags from installed .pc metadata.

  • Use pinned container/CI images for reproducible builds rather than modifying hosts during every job.

  • Security updates can change the patch version; rebuild/test native dependents according to release policy.

4. Verify the installed header and metadata

Ubuntu shellbash
dpkg -L libssl-dev | grep -E "/openssl/(ssl|opensslv)\.h$"
pkg-config --modversion openssl
pkg-config --cflags --libs openssl
Expect packaged header paths, an OpenSSL version, and link flags typically containing -lssl -lcrypto (include/library flags may be empty for default system paths).

Empty cflags can be correct

  • dpkg -L proves which installed package owns the files.

  • On a native system install, /usr/include is already a compiler default, so pkg-config may emit no -I flag.

  • The openssl pkg-config module represents both libraries for typical TLS consumers.

  • Do not confuse /usr/include/openssl/ssl.h with NSS’s differently located ssl.h.

  • If metadata selects /usr/local, a custom installation may be shadowing Ubuntu packages.

Directory containing openssl_probe.cbash
cc -std=c17 -Wall -Wextra -Wpedantic \
  -o openssl_probe openssl_probe.c \
  $(pkg-config --cflags --libs openssl)
./openssl_probe
The program should print the runtime OpenSSL version selected by the loader.

Let metadata carry platform paths

  • Command substitution injects flags reported by the trusted local pkg-config database.

  • Source/object files appear before -lssl -lcrypto, which matters for one-pass/static linkers.

  • libssl depends on libcrypto; the metadata preserves the expected relationship/order.

  • Do not pass untrusted PKG_CONFIG_PATH or shell content into a privileged build.

  • For production, record compiler, flags, pkg-config version, linked artifacts, and runtime loader resolution.

6. Use imported CMake targets

CMakeLists.txtcmake
cmake_minimum_required(VERSION 3.20)
project(openssl_probe LANGUAGES C)
 
find_package(OpenSSL REQUIRED COMPONENTS SSL)
 
add_executable(openssl_probe openssl_probe.c)
target_compile_features(openssl_probe PRIVATE c_std_17)
target_link_libraries(openssl_probe PRIVATE OpenSSL::SSL)

OpenSSL::SSL carries usage requirements

  • REQUIRED stops configuration with a clear failure instead of producing a broken target.

  • Requesting component SSL ensures the TLS library is present.

  • OpenSSL::SSL carries include/link requirements and also links OpenSSL::Crypto as documented by CMake.

  • Target-scoped dependencies avoid leaking flags globally.

  • Specify a supported version/range when the source requires a particular OpenSSL API.

CMake project rootbash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --verbose
./build/openssl_probe
CMake should report the found OpenSSL installation/version, compile the target, and the executable should print its runtime version.

A stale cache can pin the wrong installation

  • -S and -B keep generated files out of source.

  • Verbose output reveals actual include directories and libraries.

  • If OpenSSL was moved/upgraded, use a fresh build directory rather than editing cache internals.

  • OPENSSL_ROOT_DIR is a hint for intentional custom installations; do not set it blindly.

  • Cross builds need a toolchain/sysroot configuration, not host /usr paths.

If libssl-dev is installed but the header is still missing

  • Run the failing compile with verbose/preprocessor include tracing to see real search paths.

  • Check whether -nostdinc, an isolated sysroot, container, chroot, snap, or remote build excludes host headers.

  • Confirm the compiler architecture/target matches the installed development package.

  • Inspect PKG_CONFIG_PATH, PKG_CONFIG_LIBDIR, CMake cache, OPENSSL_ROOT_DIR, and custom CPATH/include flags.

  • Look for /usr/local/include/openssl or vendored headers shadowing the distro version.

  • Ensure the build command runs in the same environment where the package was installed.

Cross-compilation requires target headers

Installing native amd64 libssl-dev does not make those headers/libraries correct for an ARM sysroot. Use target-architecture packages or build/install OpenSSL into the target sysroot, configure pkg-config/CMake to search only that sysroot, and prevent host libraries from leaking into the link.

  • Headers must match the target library ABI/configuration.

  • Multiarch package syntax and availability depend on enabled architectures/repositories.

  • Set compiler target, sysroot, pkg-config sysroot/library directories, and CMake toolchain coherently.

  • Never “fix” cross builds with a raw -I/usr/include or -L/usr/lib host escape.

  • Run artifact inspection and target/emulator tests before release.

Container builds need a build stage

Dockerfile (Debian/Ubuntu-based example)dockerfile
FROM ubuntu:24.04 AS build
RUN apt-get update \
 && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
      build-essential libssl-dev pkg-config \
 && rm -rf /var/lib/apt/lists/*
WORKDIR /src
COPY . .
RUN cc -std=c17 -O2 -o openssl_probe openssl_probe.c \
    $(pkg-config --cflags --libs openssl)

Keep compilers and headers out of the runtime image

  • Pin the base image by digest and use a trusted update/rebuild policy for reproducibility and security.

  • A multi-stage runtime should copy only the executable and require the compatible runtime library package.

  • Dynamic linkage means the final image still needs matching libssl/libcrypto runtime libraries.

  • Static linking changes licensing, update, provider/module, NSS/certificate, and vulnerability-patching responsibilities.

  • Do not bake credentials or private package tokens into image layers.

Header fixed, but undefined references remain

  • Add -lssl -lcrypto through pkg-config, or link CMake OpenSSL::SSL.

  • Place libraries after source/object files for linkers that resolve left to right.

  • Do not use only -lcrypto for APIs implemented in libssl.

  • With static archives, additional dependencies and group/order rules may be required; use metadata.

  • Confirm C and C++ compiler/link driver consistency and ABI/toolchain compatibility.

Header and library version mismatch

Built executable directorybash
pkg-config --modversion openssl
ldd ./openssl_probe | grep -E "lib(ssl|crypto)"
./openssl_probe
Compare the build metadata version, loader-selected shared objects, and program-reported runtime version.

Compilation success can hide runtime selection

  • ldd is appropriate for a trusted local binary; do not run it on untrusted executables because implementations may execute code.

  • RPATH/RUNPATH, LD_LIBRARY_PATH, /usr/local, containers, and loader cache influence selection.

  • OpenSSL major versions can change/deprecate APIs and ABI names.

  • Remove unintended custom installations or configure an intentional isolated prefix consistently.

  • Never copy random .so files into system directories to silence a loader error.

OpenSSL 3 migration is a separate task

  • Installing current headers may expose deprecated low-level algorithms/APIs in old source.

  • Prefer documented high-level EVP interfaces for cryptographic operations.

  • OpenSSL 3 providers replace many legacy engine/algorithm-loading assumptions.

  • The legacy provider is not a blanket production fix and does not make obsolete cryptography safe.

  • Read the migration guide, update code, test protocol/certificate/provider behavior, and define supported versions explicitly.

Common error map

  • openssl/ssl.h missing: install target-correct development files or fix include/sysroot discovery.

  • Package installed, compiler still fails: build runs in another container/sysroot/architecture or uses -nostdinc/wrong flags.

  • undefined reference to SSL_*: libssl is not linked or appears in the wrong order.

  • undefined reference to EVP/OPENSSL_*: libcrypto missing/order/version mismatch.

  • CMake could not find OpenSSL: stale cache, wrong prefix/toolchain/sysroot, or development package absent.

  • Wrong version found: custom /usr/local, environment metadata, cache, or runtime loader overrides distro installation.

  • Works locally, fails CI: CI image lacks libssl-dev/pkg-config or uses a different Ubuntu/OpenSSL/toolchain version.

  • Runtime cannot open libssl.so: runtime package/loader path/ABI differs from build environment.

Verification checklist

  • The first failing stage—preprocess, compile, link, load, or API migration—is identified.

  • libssl-dev comes from an approved repository and matches target architecture/runtime.

  • Package file list and pkg-config/CMake identify the intended headers and libraries.

  • Build uses metadata/imported targets instead of hard-coded host paths.

  • Clean native and CI/container builds pass with warnings enabled.

  • Runtime loader selects the intended version and smoke/integration TLS tests pass.

  • OpenSSL security updates, supported major versions, provider/configuration, certificate trust, and rebuild ownership are documented.

Primary references