prop/Dockerfile.base
Graham McIntire 0c3be97abb
Some checks failed
Build base image / Build and push base image (push) Successful in 3m10s
Build and Push / Build and Push Docker Image (push) Failing after 14s
Build prop-grid-rs / Test, build, push (push) Successful in 12m52s
fix: resolve 27 security, architecture, test, and performance audit findings
P0 (security-critical):
- Gate CSV/ADIF upload tabs behind authentication, add 30s cooldown to all upload handlers
- Cap CSV/ADIF imports at 2,000 rows server-side in both parsers
- Add submitter_verified boolean to contacts (client-cannot-set, anonymous=false)
- Create k8s/secret.example.yaml with placeholders, add LIVE_VIEW_SIGNING_SALT

P1 (high-priority):
- Add Mox.verify_on_exit!() to valkey_test.exs
- Replace DateTime.utc_now() truncation with static ~U literals in map_live_test.exs
- Replace Process.sleep with render_async in pskr_spots_live_test.exs (6 occurrences)
- Add MonitorLive.Show test coverage (4 tests: owner view, non-owner redirect, config success/error)
- Extract duct-detection and mechanism-classification logic from ContactLive.Show into Propagation.PathAnalysis
- Split ContactLive.Show render into 12 function components
- Update CLAUDE.md: remove stale ML model, mark HRDPS active, add backtest/pskr dirs
- Batch CSV import enrichment jobs via new enqueue_for_contacts/1

P2 (medium-priority):
- Set secure:true on session and remember-me cookies in production
- Change SMTP TLS from verify_none to verify_peer with public_key cacerts
- Make /metrics fail-closed in production when PROMETHEUS_AUTH_TOKEN unset
- Add RateLimiter (anon_limit:10, auth_limit:60) to /api/contacts/map
- Add content-security-policy-report-only header
- Add comment noting String.to_atom is compile-time safe in hrdps_client.ex
- Delegate duplicated haversine_km to canonical Microwaveprop.Geo.haversine_km/4
- Consolidate score-tier/color/verdict formatting into Microwaveprop.Format
- Update CLAUDE.md testing section to match actual raw-string-matching practice
- Batch HrrrPointEnqueuer Repo.insert_all calls to single round-trip
- Split weather.ex (1696→216 lines) and radio.ex (1285→54 lines) into purpose-based sub-facades

P3 (low-priority):
- Add LIVE_VIEW_SIGNING_SALT warning comment, extend filter_parameters
- Add host/community validation to snmp_client.ex
- Add raw/1 safety comment in algo_live.ex
- Add hex-audit and cargo-audit Makefile targets
- Add privacy_live smoke test
- Replace notify_listener busy-poll loop with Process.monitor/1 + assert_receive
- Add ContactCommonVolumeRadar changeset validation tests (5 tests)
2026-07-27 18:19:37 -05:00

117 lines
5 KiB
Text

# syntax=docker/dockerfile:1.6
#
# Pre-built runtime base image for the Elixir app. Contains:
# * elixir-base (hexpm/elixir + build-essential + git + hex + rebar)
# * `cdo` + `gdal-bin` (~1 GB dep tree, the slowest install)
# * `wgrib2` (compiled here from source against NCEPLIBS-g2c v2.3.0)
#
# Built by CI only when this file or the workflow changes.
# The app `Dockerfile` does `FROM prop-base` so everyday code pushes
# skip the wgrib2 compile and apt-install entirely.
#
# Bumping wgrib2 or g2c: change the ARGs below and push — the base
# workflow rebuilds and re-tags `:latest`. The app Dockerfile follows
# `:latest`, so the next app push picks it up automatically.
ARG ELIXIR_BASE_IMAGE="git.mcintire.me/graham/elixir-base:latest"
# ---- wgrib2 build stage (cached independently, uses raw debian) ----
ARG DEBIAN_VERSION=trixie-20260518-slim
FROM docker.io/debian:${DEBIAN_VERSION} AS wgrib2-builder
ARG WGRIB2_VERSION=3.8.0
# wgrib2 3.6.0 has a memory-corruption bug exposed by HRDPS rotated
# lat/lon GRIB2 files plus -lon point extraction (denormal garbage
# values, then `free(): invalid size`). Reproduced in the production
# pod against an HRDPS sample on 2026-04-30; 3.8.0 fixes it.
# g2c v2.3.0 is the minimum wgrib2 3.8.0's CMakeLists demands via
# find_package, and the first release where BUILD_G2C defaults ON.
ARG G2C_VERSION=2.3.0
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
rm -f /etc/apt/apt.conf.d/docker-clean \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential gfortran cmake wget ca-certificates pkg-config \
zlib1g-dev libaec-dev libpng-dev libopenjp2-7-dev
# NCEPLIBS-g2c carries the actual JPEG2000 / PNG / AEC GRIB2 decoders
# that wgrib2 dispatches to via USE_G2CLIB_LOW. Jasper is disabled
# (CVE-laden, dropped from Debian).
WORKDIR /tmp/g2c
RUN wget -q --content-disposition "https://github.com/NOAA-EMC/NCEPLIBS-g2c/archive/refs/tags/v${G2C_VERSION}.tar.gz" \
-O g2c.tar.gz \
&& tar xzf g2c.tar.gz \
&& cd NCEPLIBS-g2c-${G2C_VERSION} \
&& mkdir build && cd build \
&& cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local \
-DCMAKE_BUILD_TYPE=Release \
-DUSE_PNG=ON \
-DUSE_AEC=ON \
-DUSE_OpenJPEG=ON \
-DUSE_Jasper=OFF \
&& make -j$(nproc) \
&& make install
WORKDIR /tmp/wgrib2
RUN wget -q --content-disposition "https://github.com/NOAA-EMC/wgrib2/archive/refs/tags/v${WGRIB2_VERSION}.tar.gz" \
-O wgrib2.tar.gz \
&& tar xzf wgrib2.tar.gz \
&& cd wgrib2-${WGRIB2_VERSION} \
&& mkdir build && cd build \
&& cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_BUILD_TYPE=Release \
-DUSE_AEC=ON -DUSE_G2CLIB_LOW=ON \
&& make -j$(nproc) \
&& make install \
&& strip /usr/local/bin/wgrib2
# ---- Final runtime base ----
# elixir-base already carries Elixir + Erlang + BEAM runtime apt deps
# (libstdc++6, openssl, libncurses6, libsctp1, ca-certificates).
# We layer cdo + gdal-bin + wgrib2 deps + locale on top.
FROM ${ELIXIR_BASE_IMAGE} AS base
# Weekly cron passes CACHE_BUST=<iso year+week> so the apt layers below
# re-execute and pick up Debian security patches. Source-controlled
# pushes leave the value at its `none` default, so day-to-day base
# rebuilds (Dockerfile.base edits) don't pay the apt-update cost
# unless the week has rolled over.
#
# Note: the registry occasionally GCs base-image blobs that the
# manifest still references (saw this 2026-05-04 — `final 1/5 FROM
# prop-base:latest` failed with "could not fetch content descriptor
# … not found"). When that happens, push any change to this file to
# re-trigger the build-base workflow and re-upload the layers.
ARG CACHE_BUST=none
# Extra runtime apt deps: snmp (app), wgrib2 deps (gfortran, aec, zlib,
# png, openjpeg), plus cdo + gdal-bin.
# cdo + gdal-bin are split into a second RUN so a transient cdo install
# failure doesn't invalidate the smaller base layer above.
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
echo "cache-bust=${CACHE_BUST}" \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
locales snmp libgfortran5 libaec0 zlib1g libpng16-16t64 libopenjp2-7
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
echo "cache-bust=${CACHE_BUST}" \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& apt-get update \
&& apt-get install -y --no-install-recommends cdo gdal-bin
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en
ENV LC_ALL=en_US.UTF-8
# wgrib2 binary + dynamic g2c lib. ldconfig refreshes the loader cache
# so /usr/local/lib is searched at exec time without LD_LIBRARY_PATH.
COPY --from=wgrib2-builder /usr/local/bin/wgrib2 /usr/local/bin/wgrib2
COPY --from=wgrib2-builder /usr/local/lib/libg2c.so* /usr/local/lib/
RUN ldconfig