commit 602ffa08f68efec0aab41e301472484736594abf Author: Chewbaccalakis Date: Sat Sep 19 14:05:40 2026 -0700 first set of images diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..71739ac --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +.gitea +certs/.gitkeep +scripts/refresh-certs.sh +README.md +.gitignore diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..24a336b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +*.sh text eol=lf +*.yml text eol=lf +docker-bake.hcl text eol=lf +Dockerfile text eol=lf diff --git a/.gitea/workflows/container.yml b/.gitea/workflows/container.yml new file mode 100644 index 0000000..df4a1fd --- /dev/null +++ b/.gitea/workflows/container.yml @@ -0,0 +1,69 @@ +name: Container + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + workflow_dispatch: + +permissions: + contents: read + packages: write + +concurrency: + group: container-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Fetch AtlasHorizon CAs + run: sh scripts/refresh-certs.sh + + - name: Registry and image names + run: | + REGISTRY="${{ vars.REGISTRY }}" + if [ -z "$REGISTRY" ]; then + REGISTRY="${GITHUB_SERVER_URL#https://}" + REGISTRY="${REGISTRY#http://}" + fi + IMAGE="${REGISTRY}/${GITHUB_REPOSITORY,,}" + echo "REGISTRY=${REGISTRY}" >> "$GITHUB_ENV" + echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV" + echo "SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)" >> "$GITHUB_ENV" + if [ "${{ github.ref }}" = "refs/heads/${{ github.event.repository.default_branch }}" ]; then + echo "LATEST=true" >> "$GITHUB_ENV" + else + echo "LATEST=false" >> "$GITHUB_ENV" + fi + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "MANUAL=true" >> "$GITHUB_ENV" + else + echo "MANUAL=false" >> "$GITHUB_ENV" + fi + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to the Gitea Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.REGISTRY_USERNAME || github.actor }} + password: ${{ secrets.REGISTRY_TOKEN || secrets.GITEA_TOKEN }} + + - name: Build and push + uses: docker/bake-action@v6 + env: + IMAGE: ${{ env.IMAGE }} + LATEST: ${{ env.LATEST }} + SHA: ${{ env.SHA }} + MANUAL: ${{ env.MANUAL }} + with: + files: docker-bake.hcl + push: ${{ github.event_name != 'pull_request' }} + provenance: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fbe62ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +certs/*.crt +certs/*.tmp diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1b588bb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +ARG BASE_IMAGE=debian:trixie-slim +FROM ${BASE_IMAGE} + +COPY certs/*.crt /tmp/atlas-ca/ +COPY scripts/install-ca.sh /tmp/install-ca.sh +RUN sh /tmp/install-ca.sh && rm -rf /tmp/install-ca.sh /tmp/atlas-ca diff --git a/README.md b/README.md new file mode 100644 index 0000000..10b260d --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# AtlasHorizon PKI-trusted base images + +Upstream OS bases with the AtlasHorizon PKI installed in the native system trust store. That is the only change. + +Images are built from current CAs fetched at build/CI time from [http://pki.atlashorizon.net/certs/](http://pki.atlashorizon.net/certs/). Generated PEMs are not committed. + +## Images + +| Image | Upstream | +| --- | --- | +| `git.atlashorizon.net/atlashorizon/container-base/debian:trixie` | `debian:trixie` | +| `git.atlashorizon.net/atlashorizon/container-base/debian-slim:trixie` | `debian:trixie-slim` | +| `git.atlashorizon.net/atlashorizon/container-base/ubuntu:24.04` | `ubuntu:24.04` | +| `git.atlashorizon.net/atlashorizon/container-base/alpine:3.24` | `alpine:3.24` | + +On `main`, each family also gets a `:latest` tag (`debian:latest`, `debian-slim:latest`, `ubuntu:latest`, `alpine:latest`). + +```dockerfile +FROM git.atlashorizon.net/atlashorizon/container-base/debian-slim:trixie +FROM git.atlashorizon.net/atlashorizon/container-base/ubuntu:24.04 +FROM git.atlashorizon.net/atlashorizon/container-base/alpine:3.24 +``` + +## Certificates + +`scripts/refresh-certs.sh` downloads and converts (DER → PEM when needed): + +- Root: `http://pki.atlashorizon.net/certs/AtlasHorizonRootCA.crt` (never `/root.crt`) +- Enterprise: `http://pki.atlashorizon.net/certs/AtlasHorizon%20Enterprise%20CA.crt` +- Issuing: `http://pki.atlashorizon.net/certs/AtlasHorizon%20Issuing%20CA.crt` + +into `certs/AtlasHorizon-*-CA.crt`. Those files are gitignored. The Gitea Action runs the script after checkout so every image build embeds the current CAs. + +Locally: + +```bash +sh scripts/refresh-certs.sh +docker buildx bake +``` + +Requires `curl` and `openssl`. + +## Gitea Actions and registry + +[`.gitea/workflows/container.yml`](.gitea/workflows/container.yml) fetches the CAs, builds all four images with Bake, and publishes to this Gitea instance’s OCI registry. + +On `main`, version tags (`v*`), and manual **Run workflow**: + +``` +git.atlashorizon.net/atlashorizon/container-base/debian:trixie +git.atlashorizon.net/atlashorizon/container-base/debian:latest # default branch only +git.atlashorizon.net/atlashorizon/container-base/debian:sha- +git.atlashorizon.net/atlashorizon/container-base/debian:manual # workflow_dispatch +``` + +(and the same tag set for `debian-slim`, `ubuntu`, `alpine`). + +The host is taken from the Actions server URL (or `vars.REGISTRY` if you set one). The image prefix is `{registry}/{owner}/{repo}` in lowercase, which is what [Gitea’s container registry](https://docs.gitea.com/usage/packages/container) requires. + +Pull requests **build** the images but do **not** push. + +### One-time Gitea setup + +1. Enable **Actions** on the repository (`Settings` → `Actions`). +2. Enable **Packages** for the owner (user or org) if it is not already on. +3. Register an **act_runner** that can build images: Docker socket or privileged `dind`. A label matching `runs-on: ubuntu-latest` is enough. +4. If `actions/checkout@v4` and the Docker actions cannot be fetched, either set Gitea `[actions].DEFAULT_ACTIONS_URL = github` or mirror those actions onto this instance and change the `uses:` lines. + +The workflow logs in as the user who triggered the job (`github.actor`) with `secrets.GITEA_TOKEN`. Give that token **package write** if the instance uses restricted token permissions. + +If login returns **401**, create a PAT with **package read/write** and add repository secrets: + +| Secret | Purpose | +| --- | --- | +| `REGISTRY_TOKEN` | PAT used instead of `GITEA_TOKEN` | +| `REGISTRY_USERNAME` | Optional; defaults to the triggering user | + +| Variable | Purpose | +| --- | --- | +| `REGISTRY` | Registry host if it is not the Gitea web hostname | + +```bash +docker login git.atlashorizon.net +docker pull git.atlashorizon.net/atlashorizon/container-base/debian-slim:trixie +``` diff --git a/certs/.gitkeep b/certs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 0000000..b55d499 --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,61 @@ +variable "IMAGE" { + default = "git.atlashorizon.net/atlashorizon/container-base" +} + +variable "LATEST" { + default = "false" +} + +variable "SHA" { + default = "" +} + +variable "MANUAL" { + default = "false" +} + +function "image_tags" { + params = [family, version] + result = compact([ + "${IMAGE}/${family}:${version}", + equal(LATEST, "true") ? "${IMAGE}/${family}:latest" : "", + notequal(SHA, "") ? "${IMAGE}/${family}:sha-${SHA}" : "", + equal(MANUAL, "true") ? "${IMAGE}/${family}:manual" : "", + ]) +} + +group "default" { + targets = ["debian", "debian-slim", "ubuntu", "alpine"] +} + +target "debian" { + dockerfile = "Dockerfile" + args = { + BASE_IMAGE = "debian:trixie" + } + tags = image_tags("debian", "trixie") +} + +target "debian-slim" { + dockerfile = "Dockerfile" + args = { + BASE_IMAGE = "debian:trixie-slim" + } + tags = image_tags("debian-slim", "trixie") +} + +target "ubuntu" { + dockerfile = "Dockerfile" + args = { + BASE_IMAGE = "ubuntu:24.04" + } + tags = image_tags("ubuntu", "24.04") +} + +target "alpine" { + dockerfile = "Dockerfile" + args = { + BASE_IMAGE = "alpine:3.24" + } + tags = image_tags("alpine", "3.24") +} diff --git a/scripts/install-ca.sh b/scripts/install-ca.sh new file mode 100644 index 0000000..6d5de5b --- /dev/null +++ b/scripts/install-ca.sh @@ -0,0 +1,67 @@ +#!/bin/sh +# Install AtlasHorizon CAs into the OS trust store (Debian/Ubuntu apt or Alpine apk). +set -eu + +SRC_DIR="/tmp/atlas-ca" +CERT_DIR="/usr/local/share/ca-certificates" +BUNDLE="/etc/ssl/certs/ca-certificates.crt" +EXPECTED=3 + +count=0 +for cert in "${SRC_DIR}"/*.crt; do + [ -f "${cert}" ] || continue + count=$((count + 1)) +done + +if [ "${count}" -ne "${EXPECTED}" ]; then + echo "expected ${EXPECTED} CA files in ${SRC_DIR}, found ${count}" >&2 + exit 1 +fi + +# Install the package first so its postinst does not consume our CAs yet. +if command -v apk >/dev/null 2>&1; then + apk add --no-cache ca-certificates +elif command -v apt-get >/dev/null 2>&1; then + export DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get install -y --no-install-recommends ca-certificates + rm -rf /var/lib/apt/lists/* +else + echo "unsupported base image: need apk or apt-get" >&2 + exit 1 +fi + +mkdir -p "${CERT_DIR}" +cp "${SRC_DIR}"/*.crt "${CERT_DIR}/" + +output="$(update-ca-certificates 2>&1)" +printf '%s\n' "${output}" + +case "${output}" in + *" added"*) + added="$(printf '%s\n' "${output}" | sed -n 's/.*\([0-9][0-9]*\) added.*/\1/p' | head -n 1)" + if [ -z "${added}" ] || [ "${added}" -lt "${EXPECTED}" ]; then + echo "update-ca-certificates added ${added:-0} CAs, expected ${EXPECTED}" >&2 + exit 1 + fi + ;; +esac + +if [ ! -f "${BUNDLE}" ]; then + echo "missing trust bundle ${BUNDLE}" >&2 + exit 1 +fi + +for cert in "${CERT_DIR}"/*.crt; do + body="$(sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p' "${cert}" | sed '1d;$d' | head -n 1)" + if [ -z "${body}" ]; then + echo "could not read PEM body from ${cert}" >&2 + exit 1 + fi + if ! grep -q -F "${body}" "${BUNDLE}"; then + echo "CA not present in ${BUNDLE}: ${cert}" >&2 + exit 1 + fi +done + +echo "AtlasHorizon PKI: ${EXPECTED} CAs installed in the system trust store" diff --git a/scripts/refresh-certs.sh b/scripts/refresh-certs.sh new file mode 100644 index 0000000..8282827 --- /dev/null +++ b/scripts/refresh-certs.sh @@ -0,0 +1,47 @@ +#!/bin/sh +# Download AtlasHorizon CAs from pki.atlashorizon.net/certs/ and write PEM files. +# Never fetch /root.crt — that path is deprecated. +set -eu + +ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +DEST="${ROOT}/certs" +PKI="http://pki.atlashorizon.net/certs" + +if ! command -v curl >/dev/null 2>&1; then + echo "curl is required" >&2 + exit 1 +fi +if ! command -v openssl >/dev/null 2>&1; then + echo "openssl is required" >&2 + exit 1 +fi + +mkdir -p "${DEST}" + +# Convert DER or PEM input to a normalized PEM certificate. +to_pem() { + src="$1" + dest="$2" + if grep -q "BEGIN CERTIFICATE" "${src}"; then + openssl x509 -in "${src}" -out "${dest}" + else + openssl x509 -inform DER -in "${src}" -out "${dest}" + fi +} + +fetch_pem() { + url="$1" + dest="$2" + tmp="${dest}.tmp" + echo "Fetching ${url}" + curl -fsSL "${url}" -o "${tmp}" + to_pem "${tmp}" "${dest}" + rm -f "${tmp}" + openssl x509 -in "${dest}" -noout -subject -issuer +} + +fetch_pem "${PKI}/AtlasHorizonRootCA.crt" "${DEST}/AtlasHorizon-Root-CA.crt" +fetch_pem "${PKI}/AtlasHorizon%20Enterprise%20CA.crt" "${DEST}/AtlasHorizon-Enterprise-CA.crt" +fetch_pem "${PKI}/AtlasHorizon%20Issuing%20CA.crt" "${DEST}/AtlasHorizon-Issuing-CA.crt" + +echo "Wrote AtlasHorizon CAs to ${DEST}"