Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions misc/images/openssh-static/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Copyright Materialize, Inc. and contributors. All rights reserved.
#
# Use of this software is governed by the Business Source License
# included in the LICENSE file at the root of this repository.
#
# As of the Change Date specified in that file, in accordance with
# the Business Source License, use of this software will be governed
# by the Apache License, Version 2.0.

# Build a statically-linked OpenSSH `ssh` client binary using AWS-LC
# as the crypto backend. AWS-LC is a faster, smaller alternative to
# OpenSSL that also supports FIPS 140-3 validation when needed.
#
# OpenSSH natively supports AWS-LC as a crypto backend (no patches needed).
# See: https://github.com/openssh/openssh-portable/blob/master/INSTALL
#
# To enable FIPS mode, build with: --build-arg AWS_LC_FIPS=1
# (requires Go for the FIPS delocator)
#
# Usage:
# docker build -t openssh-static .
# docker create --name extract openssh-static
# docker cp extract:/output/ssh ./ssh
# docker rm extract

FROM ubuntu:noble-20260210.1 AS builder

ARG AWS_LC_VERSION=v1.54.0
ARG AWS_LC_FIPS=0
ARG OPENSSH_VERSION=V_9_9_P2

RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
autoconf \
automake \
build-essential \
ca-certificates \
cmake \
git \
golang \
ninja-build \
perl \
pkg-config \
wget \
zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*

# Build AWS-LC as a static library.
# When AWS_LC_FIPS=1, enables FIPS mode (requires Go for the delocator).
WORKDIR /build/aws-lc
RUN git clone --depth 1 --branch ${AWS_LC_VERSION} https://github.com/aws/aws-lc.git . \
&& cmake -GNinja -B build \
$([ "$AWS_LC_FIPS" = "1" ] && echo "-DFIPS=1") \
-DBUILD_SHARED_LIBS=0 \
-DBUILD_TESTING=OFF \
-DBUILD_TOOL=OFF \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/opt/aws-lc \
-DCMAKE_C_FLAGS="-fPIC" \
&& ninja -C build \
&& ninja -C build install

# Build OpenSSH ssh client against AWS-LC.
WORKDIR /build/openssh
RUN git clone --depth 1 --branch ${OPENSSH_VERSION} https://github.com/openssh/openssh-portable.git . \
&& autoreconf \
&& ./configure \
--with-ssl-dir=/opt/aws-lc \
--with-zlib \
--with-ldflags=-static \
--without-pam \
--without-libedit \
--disable-pkcs11 \
# AWS-LC does not define the legacy OpenSSL BN_FLG_CONSTTIME flag.
# Setting it to 0 satisfies #ifdef checks in OpenSSH source code.
# This is safe: AWS-LC handles constant-time bignum operations
# internally and does not rely on this flag.
# --disable-pkcs11 avoids link errors from ssh-pkcs11.c calling
# RSA_meth_dup/EC_KEY_METHOD_get_sign which AWS-LC does not provide.
&& make -j"$(nproc)" ssh CFLAGS="-DBN_FLG_CONSTTIME=0" \
&& strip ssh

# Verify the binary is not dynamically linked and is functional.
RUN ! ldd ssh 2>/dev/null \
&& ./ssh -V

# Output stage: just the binary.
FROM scratch
COPY --from=builder /build/openssh/ssh /output/ssh
11 changes: 11 additions & 0 deletions misc/images/openssh-static/mzbuild.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright Materialize, Inc. and contributors. All rights reserved.
#
# Use of this software is governed by the Business Source License
# included in the LICENSE file at the root of this repository.
#
# As of the Change Date specified in that file, in accordance with
# the Business Source License, use of this software will be governed
# by the Apache License, Version 2.0.

name: openssh-static
description: Statically-linked OpenSSH ssh client built against AWS-LC (FIPS optional).
44 changes: 39 additions & 5 deletions src/ssh-util/src/tunnel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,28 @@ impl SshTunnelHandle {
}
}

/// Returns true if FIPS mode is enabled via the MZ_FIPS environment variable.
fn fips_mode_enabled() -> bool {
std::env::var("MZ_FIPS").map_or(false, |v| v == "1" || v == "true")
}

/// Writes a temporary SSH config file that restricts algorithms to FIPS 140-3
/// approved choices only. Returns the path to the config file.
fn write_fips_ssh_config(dir: &std::path::Path) -> Result<std::path::PathBuf, anyhow::Error> {
let config_path = dir.join("ssh_config");
let config_contents = "\
# FIPS 140-3 compliant SSH configuration.
# Only NIST-approved algorithms are permitted.
Ciphers aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
KexAlgorithms ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256,hmac-sha2-512
HostKeyAlgorithms ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,rsa-sha2-256,rsa-sha2-512
PubkeyAcceptedAlgorithms ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,rsa-sha2-256,rsa-sha2-512,ssh-ed25519
";
fs::write(&config_path, config_contents)?;
Ok(config_path)
}

async fn connect(
config: &SshTunnelConfig,
timeout_config: SshTimeoutConfig,
Expand All @@ -248,24 +270,36 @@ async fn connect(
// Mostly helpful to ensure the file is not accidentally overwritten.
tempfile.set_permissions(std::fs::Permissions::from_mode(0o400))?;

// In FIPS mode, write a restrictive SSH config that only allows
// NIST-approved algorithms.
let fips_config_path = if fips_mode_enabled() {
Some(write_fips_ssh_config(tempdir.path())?)
} else {
None
};

// Try connecting to each host in turn.
let mut connect_err = None;
for host in &config.host {
// Bastion hosts (and therefore keys) tend to change, so we don't want
// to lock ourselves into trusting only the first we see. In any case,
// recording a known host would only last as long as the life of a
// storage pod, so it doesn't offer any protection.
match openssh::SessionBuilder::default()
let mut builder = openssh::SessionBuilder::default();
builder
.known_hosts_check(openssh::KnownHosts::Accept)
.user_known_hosts_file("/dev/null")
.user(config.user.clone())
.port(config.port)
.keyfile(&path)
.server_alive_interval(timeout_config.keepalives_idle)
.connect_timeout(timeout_config.connect_timeout)
.connect_mux(host.clone())
.await
{
.connect_timeout(timeout_config.connect_timeout);

if let Some(ref fips_config) = fips_config_path {
builder.config_file(fips_config);
}

match builder.connect_mux(host.clone()).await {
Ok(session) => {
// Delete the private key for safety: since `ssh` still has an open
// handle to it, it still has access to the key.
Expand Down
Loading