diff --git a/Dockerfile b/Dockerfile index d78a235..b707c3b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,9 +2,12 @@ ARG ELIXIR_VERSION=1.18.4 ARG OTP_VERSION=27.2.4 ARG DEBIAN_VERSION=bullseye-20250520-slim +# Set default non-root user and group IDs +ARG USER_ID=1000 +ARG GROUP_ID=1000 + ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" -# Use distroless base image - this includes glibc and essential libraries -ARG RUNNER_IMAGE="gcr.io/distroless/cc-debian11:nonroot" +ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}" FROM ${BUILDER_IMAGE} AS builder @@ -31,11 +34,15 @@ RUN mix deps.get --only $MIX_ENV RUN mkdir config # copy compile-time config files before we compile dependencies +# to ensure any relevant config change will trigger the dependencies +# to be re-compiled. COPY config/config.exs config/${MIX_ENV}.exs config/ RUN mix deps.compile COPY priv priv + COPY lib lib + COPY assets assets # compile assets @@ -48,36 +55,88 @@ RUN mix compile COPY config/runtime.exs config/ COPY rel rel -RUN mix release && \ - chmod +x /app/_build/prod/rel/aprs/bin/server /app/_build/prod/rel/aprs/bin/aprs +RUN mix release -# Final distroless stage +# start a new build stage so that the final image will only contain +# the compiled release and other runtime necessities FROM ${RUNNER_IMAGE} -# Set environment variables for production +# Install security updates and required packages +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update -y && \ + apt-get upgrade -y && \ + apt-get install -y --no-install-recommends \ + libstdc++6 \ + openssl \ + libncurses5 \ + locales \ + ca-certificates \ + tini && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ + # Create a non-root user and group with specific ID + groupadd -g 1000 aprs && \ + useradd -r -g aprs -u 1000 -s /bin/false -M aprs && \ + # Remove setuid and setgid permissions + find / -perm /6000 -type f -exec chmod a-s {} \; || true && \ + # Secure system configurations + chmod 0600 /etc/login.defs && \ + chmod 0600 /etc/passwd && \ + chmod 0600 /etc/group && \ + # Create and secure app directory + mkdir -p /app && \ + chown aprs:aprs /app && \ + chmod 0750 /app + +# Set the locale +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 + +WORKDIR "/app" + +# Set security-related environment variables ENV MIX_ENV="prod" \ - LANG=C.UTF-8 \ - # Disable history files for security + LANG=en_US.UTF-8 \ + LANGUAGE=en_US:en \ + LC_ALL=en_US.UTF-8 \ + # Disable history files HISTFILE=/dev/null \ - # Set HOME to /tmp (writable in distroless) - HOME=/tmp \ - # Configure Erlang VM - ERL_AFLAGS="+S 1:1 +A 1 +K true" \ + # Prevent writing .erlang.cookie file + HOME=/dev/null \ + # Add security headers + SECURITY_HEADERS="true" \ + # Disable debug info in production + ERL_AFLAGS="+S 1:1 +A 1 +K true -kernel shell_history enabled -kernel shell_history_file_bytes 0" \ PHX_SERVER=true -WORKDIR /app +# Only copy the final release from the build stage +COPY --from=builder --chown=1000:1000 /app/_build/${MIX_ENV}/rel/aprs ./ -# Copy the Elixir release from builder stage -# nonroot user in distroless has UID 65532 -COPY --from=builder --chown=65532:65532 /app/_build/prod/rel/aprs ./ +USER 1000 -# Add security metadata +# Ensure the server binary is executable +RUN chmod +x /app/bin/server + +# If using an environment that doesn't automatically reap zombie processes, it is +# advised to add an init process such as tini via `apt-get install` +# above and adding an entrypoint. See https://github.com/krallin/tini for details +# ENTRYPOINT ["/tini", "--"] + +# Add specific capabilities needed by the app +CMD /app/bin/server + +# Add security-related metadata LABEL org.opencontainers.image.vendor="APRS.me" \ - org.opencontainers.image.title="APRS.me Server (Distroless)" \ - org.opencontainers.image.description="APRS.me server with distroless base" \ - security.distroless="true" \ - security.nonroot="true" + org.opencontainers.image.title="APRS.me Server" \ + org.opencontainers.image.description="APRS.me server with security hardening" \ + org.opencontainers.image.version="${MIX_ENV}" \ + org.opencontainers.image.created="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ + security.root-user="false" \ + security.non-root-user="app" \ + security.privileged="false" -# Run the server -ENTRYPOINT ["/app/bin/server"] +# Container configuration is handled by Dokku EXPOSE $PORT diff --git a/Dockerfile.working b/Dockerfile.working new file mode 100644 index 0000000..b707c3b --- /dev/null +++ b/Dockerfile.working @@ -0,0 +1,142 @@ +ARG ELIXIR_VERSION=1.18.4 +ARG OTP_VERSION=27.2.4 +ARG DEBIAN_VERSION=bullseye-20250520-slim + +# Set default non-root user and group IDs +ARG USER_ID=1000 +ARG GROUP_ID=1000 + +ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" +ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}" + +FROM ${BUILDER_IMAGE} AS builder + +# install build dependencies +RUN apt-get update -y && \ + apt-get upgrade -y && \ + apt-get install -y build-essential git && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# prepare build dir +WORKDIR /app + +# install hex + rebar +RUN mix local.hex --force && \ + mix local.rebar --force + +# set build ENV +ENV MIX_ENV="prod" + +# install mix dependencies +COPY mix.exs mix.lock ./ +RUN mix deps.get --only $MIX_ENV +RUN mkdir config + +# copy compile-time config files before we compile dependencies +# to ensure any relevant config change will trigger the dependencies +# to be re-compiled. +COPY config/config.exs config/${MIX_ENV}.exs config/ +RUN mix deps.compile + +COPY priv priv + +COPY lib lib + +COPY assets assets + +# compile assets +RUN mix assets.deploy + +# Compile the release +RUN mix compile + +# Changes to config/runtime.exs don't require recompiling the code +COPY config/runtime.exs config/ + +COPY rel rel +RUN mix release + +# start a new build stage so that the final image will only contain +# the compiled release and other runtime necessities +FROM ${RUNNER_IMAGE} + +# Install security updates and required packages +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update -y && \ + apt-get upgrade -y && \ + apt-get install -y --no-install-recommends \ + libstdc++6 \ + openssl \ + libncurses5 \ + locales \ + ca-certificates \ + tini && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ + # Create a non-root user and group with specific ID + groupadd -g 1000 aprs && \ + useradd -r -g aprs -u 1000 -s /bin/false -M aprs && \ + # Remove setuid and setgid permissions + find / -perm /6000 -type f -exec chmod a-s {} \; || true && \ + # Secure system configurations + chmod 0600 /etc/login.defs && \ + chmod 0600 /etc/passwd && \ + chmod 0600 /etc/group && \ + # Create and secure app directory + mkdir -p /app && \ + chown aprs:aprs /app && \ + chmod 0750 /app + +# Set the locale +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 + +WORKDIR "/app" + +# Set security-related environment variables +ENV MIX_ENV="prod" \ + LANG=en_US.UTF-8 \ + LANGUAGE=en_US:en \ + LC_ALL=en_US.UTF-8 \ + # Disable history files + HISTFILE=/dev/null \ + # Prevent writing .erlang.cookie file + HOME=/dev/null \ + # Add security headers + SECURITY_HEADERS="true" \ + # Disable debug info in production + ERL_AFLAGS="+S 1:1 +A 1 +K true -kernel shell_history enabled -kernel shell_history_file_bytes 0" \ + PHX_SERVER=true + +# Only copy the final release from the build stage +COPY --from=builder --chown=1000:1000 /app/_build/${MIX_ENV}/rel/aprs ./ + +USER 1000 + +# Ensure the server binary is executable +RUN chmod +x /app/bin/server + +# If using an environment that doesn't automatically reap zombie processes, it is +# advised to add an init process such as tini via `apt-get install` +# above and adding an entrypoint. See https://github.com/krallin/tini for details +# ENTRYPOINT ["/tini", "--"] + +# Add specific capabilities needed by the app +CMD /app/bin/server + +# Add security-related metadata +LABEL org.opencontainers.image.vendor="APRS.me" \ + org.opencontainers.image.title="APRS.me Server" \ + org.opencontainers.image.description="APRS.me server with security hardening" \ + org.opencontainers.image.version="${MIX_ENV}" \ + org.opencontainers.image.created="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ + security.root-user="false" \ + security.non-root-user="app" \ + security.privileged="false" + +# Container configuration is handled by Dokku +EXPOSE $PORT