Repository navigation
Expand file tree
/
Copy pathContainerfile
More file actions
142 lines (121 loc) · 7.77 KB
/
Copy pathContainerfile
File metadata and controls
142 lines (121 loc) · 7.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# ── Builder stage ───────────────────────────────────────────────────
# Hardened, RPM-based Python builder image from a public registry
# (registry.access.redhat.com needs no credentials). Pinned by digest.
#
# MUST stay on the same Python minor as the runtime stage below: the .venv
# built here is copied wholesale into the runtime, and native-extension .so
# files are ABI-pinned per minor while the venv's script shebangs hardcode
# the builder's interpreter path.
FROM registry.access.redhat.com/hi/python:3.12-builder@sha256:d7e47ff8d9d9b612eaefb5925fcfd36e38c5ce622dc7f0f0508b4fdd090a1510 AS builder
ARG UV_VERSION=0.7
USER 0
# git is required for uv to resolve the git-tag dependency pins
# (traust-contracts). Installed via microdnf (the base image is RPM-based).
RUN microdnf install -y git ca-certificates \
&& microdnf clean all
RUN pip install --no-cache-dir "uv>=${UV_VERSION}"
WORKDIR /build
# Dependency layer first — cache-friendly when only source changes.
COPY pyproject.toml uv.lock ./
# Dependency pins are public git tags and resolve without credentials.
# A deployment that builds from a private mirror of those repositories may
# pass PRIVATE_GIT_SSH_BASE (the ssh:// prefix recorded in uv.lock) and
# PRIVATE_GIT_HTTPS_HOST (the host to fetch it from over HTTPS) plus a read
# token as the mounted secret `traust-ledger-git-auth/token`; git's insteadOf
# rewrite then maps the pins to authenticated HTTPS for this RUN only, in a
# throwaway config the builder stage discards. PRIVATE_GIT_INSECURE_TLS=1
# additionally skips TLS verification for that one host (a mirror signed by
# a CA the base image does not trust; the token remains the access control).
# Nothing here names any host; all three default to off.
ARG PRIVATE_GIT_SSH_BASE=""
ARG PRIVATE_GIT_HTTPS_HOST=""
ARG PRIVATE_GIT_INSECURE_TLS=""
RUN --mount=type=secret,id=traust-ledger-git-auth/token,required=false <<'EOF'
set -eu
if [ -n "${PRIVATE_GIT_SSH_BASE}" ] && [ -n "${PRIVATE_GIT_HTTPS_HOST}" ] \
&& [ -f /run/secrets/traust-ledger-git-auth/token ]; then
TOKEN="$(cat /run/secrets/traust-ledger-git-auth/token)"
git config --global \
"url.https://oauth2:${TOKEN}@${PRIVATE_GIT_HTTPS_HOST}/.insteadOf" \
"${PRIVATE_GIT_SSH_BASE}"
if [ "${PRIVATE_GIT_INSECURE_TLS}" = "1" ]; then
git config --global "http.https://${PRIVATE_GIT_HTTPS_HOST}/.sslVerify" false
fi
fi
uv sync --frozen --extra service --no-install-project
if [ -n "${PRIVATE_GIT_HTTPS_HOST}" ]; then
git config --global --remove-section "url.https://oauth2:${TOKEN:-}@${PRIVATE_GIT_HTTPS_HOST}/" 2>/dev/null || true
git config --global --unset "http.https://${PRIVATE_GIT_HTTPS_HOST}/.sslVerify" 2>/dev/null || true
fi
EOF
COPY . .
RUN uv build --wheel --out-dir /build/dist
# Install the built wheel into the venv so the runtime stage gets a
# single self-contained tree.
RUN uv pip install --no-deps /build/dist/*.whl
# PostgreSQL driver for the db backend (LAAS_BACKEND_TYPE=db). Deliberately
# installed only in the image, never in pyproject.toml/uv.lock, so local
# clones, uv sync, and the test suite stay driver-free (SQLite needs none).
# psycopg is LGPL-3.0-only and ships unmodified from PyPI as a separately
# replaceable library; any postgresql+psycopg:// URL requires it.
ARG PSYCOPG_VERSION=3.1
RUN uv pip install "psycopg[binary]>=${PSYCOPG_VERSION}"
# ── cosign source ───────────────────────────────────────────────────
# Take the cosign binary from a digest-pinned hardened image rather than
# downloading a release at build time: no network fetch in the build.
FROM registry.access.redhat.com/hi/cosign:latest@sha256:ea3885f93943dcd02ed956a918623a4ea1ae177267ee48861f17d55be9296249 AS cosign
# ── Runtime stage ───────────────────────────────────────────────────
# Minimal hardened Python runtime. Runs as a non-root user (UID 1001) by
# default and ships no package manager, so nothing is installed here.
# Python minor must match the builder stage above (see note there).
FROM registry.access.redhat.com/hi/python:3.12@sha256:89e3fa99f9e49fa8cde1d1d3896ec683c571538268e6cf86484b998d74147274 AS runtime
# OCI labels. VERSION and SOURCE_REVISION are supplied by the build.
ARG SOURCE_REVISION=unknown
ARG VERSION=unknown
LABEL org.opencontainers.image.source="https://github.com/traust-security/traust-ledger" \
org.opencontainers.image.revision="${SOURCE_REVISION}" \
org.opencontainers.image.title="traust-ledger" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.licenses="Apache-2.0" \
name="traust-ledger" \
version="${VERSION}" \
release="1" \
vendor="Red Hat, Inc." \
url="https://github.com/traust-security/traust-ledger" \
summary="Traust disposition-ledger service" \
description="Disposition-ledger kernel: finding identity, event math, integrity (Merkle + signing), and the single ledger write path." \
distribution-scope="public" \
com.redhat.component="traust-ledger" \
io.k8s.description="Traust disposition-ledger service." \
io.k8s.display-name="Traust Ledger Service" \
maintainer="Traust maintainers <traust@redhat.com>"
# cosign lives in the image so event-signing can call it without an
# external sidecar; the key itself never ships here.
COPY --from=cosign /usr/bin/cosign /usr/local/bin/cosign
# Red Hat certification policy (preflight HasLicense): terms and
# conditions must be present as text file(s) under /licenses.
COPY --chown=65532:65532 LICENSE /licenses/LICENSE
# --chown: the runtime base image runs as a non-root UID (65532) and the
# rewire RUN below executes as that user, so it must own the copied files.
COPY --from=builder --chown=65532:65532 /build/.venv /app/.venv
# The venv was created at /build/.venv in the builder stage, so its
# console-script shebangs (uvicorn, etc.) and `python` symlinks point at
# builder paths that do not exist in the runtime image. Rewire them to
# the runtime location. The runtime image ships no shell or package
# manager, so this runs python (by absolute path, since the venv's own
# symlinks are broken until fixed).
RUN ["/usr/bin/python3.12", "-c", "import glob, os\nvenv_bin = '/app/.venv/bin'\nfor link, target in (('python3.12', '/usr/bin/python3.12'), ('python3', 'python3.12'), ('python', 'python3.12')):\n path = os.path.join(venv_bin, link)\n if os.path.lexists(path):\n os.remove(path)\n os.symlink(target, path)\nold = '#!/build/.venv/bin/python'\nnew = '#!/app/.venv/bin/python'\nfor script in glob.glob(os.path.join(venv_bin, '*')):\n if not os.path.isfile(script) or os.path.islink(script):\n continue\n with open(script, 'rb+') as fh:\n head = fh.read(len(old) + 8)\n if head.startswith(old.encode()):\n fh.seek(0)\n fh.write(new.encode() + head[len(old):])"]
ENV PATH="/app/.venv/bin:${PATH}" \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
ARG APP_PORT=8000
EXPOSE ${APP_PORT}
HEALTHCHECK --interval=15s --timeout=3s --retries=3 \
CMD python -c "import httpx; httpx.get('http://localhost:${APP_PORT}/healthz').raise_for_status()"
# Run as the base image's non-root user, matching the file ownership above.
USER 65532
# Two worker processes: sync-route handlers run in the threadpool, but a
# second process keeps serving (and answering probes) when one is busy on a
# CPU-bound fold. Both are safe against each other — the DB backend uses
# row locks, the file backend fcntl locks.
ENTRYPOINT ["uvicorn", "traust_ledger.service.app:create_app", "--factory", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]