Skip to content

Commit 6b9faef

Browse files
[patch] Refresh base image security state
1 parent 4c4616a commit 6b9faef

11 files changed

Lines changed: 194 additions & 43 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ jobs:
162162
make push \
163163
"PROGRESS=plain" \
164164
"BUILDER=${{ steps.buildx.outputs.name }}" \
165+
"BASE_NO_CACHE=${{ inputs.image == 'base' }}" \
165166
"TARGET=${{ inputs.image }}-$PLATFORM" \
166167
"REPOSITORY=${{ inputs.repository }}" \
167168
"TAGS=${{ steps.metadata.outputs.first-tag }}" \

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ TARGET ?= default
6363
# reducing build times, etc. See the GitHub actions for an example.
6464
CONTEXTS ?=
6565

66+
# CI sets this for the base image so apk repositories are consulted on every
67+
# published rebuild instead of trusting a potentially stale registry cache.
68+
BASE_NO_CACHE ?= false
69+
6670
# All images should be included in the bake files default target.
6771
# It is the source of truth.
6872
ALL_IMAGES = $(shell docker buildx bake --print default 2>/dev/null | jq -r '.target[].context')
@@ -127,6 +131,7 @@ build/bake.json: | docker-buildx jq build folder-permissions executable-permisso
127131
BRANCH="$(BRANCH)" \
128132
CACHE_FROM_REPOSITORY=$(CACHE_FROM_REPOSITORY) \
129133
CACHE_TO_REPOSITORY=$(CACHE_TO_REPOSITORY) \
134+
BASE_NO_CACHE=$(BASE_NO_CACHE) \
130135
REPOSITORY=$(REPOSITORY) \
131136
TAGS="$(TAGS)" \
132137
docker buildx bake --print $(TARGET) 2>/dev/null > build/bake.json; \

docker-bake.hcl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,13 @@ variable "BRANCH" {
215215
default = ""
216216
}
217217

218+
variable "BASE_NO_CACHE" {
219+
# The published base image must resolve the current Alpine package index on
220+
# every CI rebuild. Otherwise BuildKit can indefinitely reuse an apk layer
221+
# after security fixes become available upstream.
222+
default = false
223+
}
224+
218225
###############################################################################
219226
# Functions
220227
###############################################################################
@@ -338,6 +345,7 @@ target "archivesspace-solr-common" {
338345
target "base-common" {
339346
inherits = ["common"]
340347
context = context("base")
348+
no-cache = BASE_NO_CACHE
341349
contexts = {
342350
# The digest (sha256 hash) is not platform specific but the digest for the manifest of all platforms.
343351
# It will be the digest printed when you do: docker pull alpine:3.17.1

images/base/Dockerfile

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ ARG \
2626
# renovate: datasource=repology depName=alpine_3_24/ca-certificates
2727
CA_CERTIFICATES_VERSION=20260611-r0 \
2828
# renovate: datasource=repology depName=alpine_3_24/curl
29-
CURL_VERSION=8.20.0-r1 \
29+
CURL_VERSION=8.21.0-r0 \
3030
# renovate: datasource=repology depName=alpine_3_24/git
3131
GIT_VERSION=2.54.0-r0 \
3232
# renovate: datasource=repology depName=alpine_3_24/gnupg
@@ -135,9 +135,10 @@ ENV \
135135
S6_SERVICES_GRACETIME=30000 \
136136
TERM=xterm
137137

138-
# CERT_AUTHORITY and CERT_PUBLIC as well as JWT_PUBLIC_KEY and JWT_PRIVATE_KEY
139-
# are defined in /etc/defaults, as Docker does not support setting multiline
140-
# environment variables via ENV.
138+
# CERT_AUTHORITY, CERT_PUBLIC, JWT_PUBLIC_KEY, and JWT_PRIVATE_KEY are defined
139+
# as empty values in /etc/defaults because Docker does not support multiline
140+
# ENV values. Deployments that use JWT authentication must inject their own
141+
# key pair through secrets or environment variables.
141142
ENV \
142143
APP_UID=100 \
143144
DB_BOOTSTRAP_ENABLED=false \

images/base/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ duplication.
7070
| Environment Variable | Default | Description |
7171
| :------------------- | :-------------------------------------------- | :----------------------------------------------------------------------------------------------------------- |
7272
| JWT_ADMIN_TOKEN | | Used for [bearer authentication]; supply a secret for services that enable Syn |
73-
| JWT_PRIVATE_KEY | @see base/rootfs/etc/defaults/JWT_PRIVATE_KEY | Private key for JWT authentication, RSA PEM Format is expected (should only be used in the Drupal container) |
74-
| JWT_PUBLIC_KEY | @see base/rootfs/etc/defaults/JWT_PUBLIC_KEY | Public key for JWT authentication |
73+
| JWT_PRIVATE_KEY | | Private key for JWT authentication, RSA PEM Format is expected (should only be used in the Drupal container) |
74+
| JWT_PUBLIC_KEY | | Public key for JWT authentication |
7575

7676
To generate a private public / private key pair use the following.
7777

@@ -82,6 +82,8 @@ ssh-keygen -q -t rsa -m pem -f /tmp/JWT -N ""
8282
This produces two files `/tmp/JWT` and `/tmp/JWT.pub`. Which can be used for
8383
`JWT_PRIVATE_KEY` and `JWT_PUBLIC_KEY` respectively. The format is RSA PEM,
8484
*without a password*. **Do not share these files** keep their contents hidden.
85+
The image intentionally contains no default JWT key pair; inject unique keys
86+
through Docker secrets (preferred) or environment variables.
8587

8688
The public/private key pair and [Syn] configuration are placed in `/opt/keys`
8789
and are only readable by the `jwt` group, if your service needs to read these files
Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1 @@
1-
-----BEGIN RSA PRIVATE KEY-----
2-
MIIEpAIBAAKCAQEA6uK3nozywVaRCAB3FHdRZNHunSZvN/c31QimZAqQMGxj7JrG
3-
h1LF8JRX+XAQ+CJcPD9r6xXjKSS1Gqa2Os2wARr/9abIwG5QeNsrJ8GMt3Z/WICn
4-
NeaFAkUVviwKWcA61iFJWvTDAuI0hCaxArRKsk0BfFSMh+4u3JAdD9tUxUx6AAUX
5-
UCdtPyluaBd53wuB0r9xRlPnDw6I9QHfKK80Xrrsu1PYATgrsy69stzCln3KlO5O
6-
xc6O8OjMdjC2D2c3HmsO4CKPvvaVuaow/a9Pa3SNje4UXN+/1xUfQskxafP8CKVS
7-
r8xxtwzSureiskb5/98moAiutpUtp15yyAm0rwIDAQABAoIBAA0PZh5OwAC4C4Bi
8-
ZjyhFcmBUr8yL+Twvg3+WSIe5D2NCVFSmc9UbuUdmnaoIIlrf61p6Vo88VCMVfWR
9-
Z3iFj0/AbJMAHxF0EM1nglLHlEdvM018ec+pbaPeq4LTeA/dfGgDmcyQ53b1lO30
10-
KMt5st2PIpIDMX0tZTWmXbdP/rqplqiQmdwH0gv8PzEG6Y2ZVLBf5viH5IvVRpg7
11-
9nDqLfe2W2ylFib+CtleX626xTUzGcJ0aqTRP1UkY4Jj5PI2/yVqttYPkvJtxZco
12-
5/14AEMcu9FMBhADCSk/0y1TkKCGsi6/VNd78AB/RrZK32HfHCwTwxoXHQoaVKq6
13-
hNQfokECgYEA+keGdVJrXMDylamARQdDe/nNgljqZZhfkKKYCGeckNqjp3iqfmld
14-
/tqCPVxAO2mIo8dNNfM2MMv6loEPx9F2REe8k9NbWFrUZ486fMHPeO7WnHt92JyU
15-
DtfJJSZ1GdCki1PthmpmP8WdoF6VpLvr5AgwuYKAzkMNth+OV/dvgqkCgYEA8EEe
16-
E3mKvePHV/PVsLt6TaqJcZEKKu6L9EgeDyzv3zz4+2zG8MVctyUFyfSl4EIC/oJy
17-
X0T5Tj1l4A3mPwZOJfQOkXnr9TaPNff1zjNx12RhUZjFtJU5V+Wn1ldtzs+XwCFc
18-
x5O/B8LWYgV4bOixNlc6tTRq/m8Txvtde9vPa5cCgYEA3OOdnxRD31QHheFYbRPx
19-
Eo0xPNae4VWvGmb2SYywmQPuplMQHot+Qvy1L9SoeAc3alzvHytta3nLy2NS+yc5
20-
+x9ZJxrGJt/bUR8PHqarJu+ch/VR54ih/8uhImGjvknvv2wuWZC0d5pA+RYheofE
21-
tLgp0MCGUATMKC4HokmmqCkCgYBEjoBTlFIn33CJw3WNyeGbefdgZb/eAlYDbfTN
22-
5cfJDvAJZr/aAqdzR2hAecQ/mvaZw4V5dAgj8Fc6uRyjjVwNbngdwQm43km9X7VP
23-
ktSAXw96Jjr8TbygPVNIUYhvBEPMOnjsJlfTkiB0thToFvpChF+nR37kfbPKCv5h
24-
Epc8nwKBgQCdKyLi54Fm24nqEuZYbAxxGI9TVT7wJjoKGn64JWrXtX7xRltmJC3t
25-
nLwNCojcbyG4kVB+Myzr2OEtFkO45j83GjrZ4O+jCuSj+AmCxEcc7xNA9cgu9usG
26-
sQXdGmIIB0Cbk54OyHNdsZgZCXi9GTRF9uvYZKL9qktS+UZMJ1Xz/g==
27-
-----END RSA PRIVATE KEY-----
1+
Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1 @@
1-
-----BEGIN PUBLIC KEY-----
2-
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6uK3nozywVaRCAB3FHdR
3-
ZNHunSZvN/c31QimZAqQMGxj7JrGh1LF8JRX+XAQ+CJcPD9r6xXjKSS1Gqa2Os2w
4-
ARr/9abIwG5QeNsrJ8GMt3Z/WICnNeaFAkUVviwKWcA61iFJWvTDAuI0hCaxArRK
5-
sk0BfFSMh+4u3JAdD9tUxUx6AAUXUCdtPyluaBd53wuB0r9xRlPnDw6I9QHfKK80
6-
Xrrsu1PYATgrsy69stzCln3KlO5Oxc6O8OjMdjC2D2c3HmsO4CKPvvaVuaow/a9P
7-
a3SNje4UXN+/1xUfQskxafP8CKVSr8xxtwzSureiskb5/98moAiutpUtp15yyAm0
8-
rwIDAQAB
9-
-----END PUBLIC KEY-----
1+

images/fcrepo6/tests/SynAuthentication/docker-compose.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ x-common: &common
44
restart: "no"
55

66
name: fcrepo6-synauthentication
7+
8+
volumes:
9+
jwt-keys:
10+
711
services:
812
activemq:
913
<<: *common
@@ -18,12 +22,29 @@ services:
1822
DB_NAME: fcrepo
1923
DB_USER: fcrepo
2024
DB_PASSWORD: test-database-password
25+
jwt-keygen:
26+
<<: *common
27+
image: ${FCREPO6:-libops/fcrepo:local-6}
28+
entrypoint:
29+
- /bin/sh
30+
- -ec
31+
command:
32+
- |
33+
umask 077
34+
openssl genrsa -out /run/secrets/JWT_PRIVATE_KEY 2048
35+
openssl rsa \
36+
-in /run/secrets/JWT_PRIVATE_KEY \
37+
-pubout \
38+
-out /run/secrets/JWT_PUBLIC_KEY
39+
volumes:
40+
- jwt-keys:/run/secrets
2141
fcrepo:
2242
environment:
2343
DB_PASSWORD: test-database-password
2444
JWT_ADMIN_TOKEN: islandora
2545
volumes:
2646
- ./test.sh:/test.sh # Test to run.
47+
- jwt-keys:/run/secrets:ro
2748
command:
2849
- /test.sh # Run test and exit.
2950
image: ${FCREPO6:-libops/fcrepo:local-6}
@@ -32,3 +53,5 @@ services:
3253
condition: service_started
3354
mariadb:
3455
condition: service_healthy
56+
jwt-keygen:
57+
condition: service_completed_successfully

images/fcrepo7/tests/SynAuthentication/docker-compose.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ x-common: &common
44
restart: "no"
55

66
name: fcrepo7-synauthentication
7+
8+
volumes:
9+
jwt-keys:
10+
711
services:
812
activemq:
913
<<: *common
@@ -18,12 +22,29 @@ services:
1822
DB_NAME: fcrepo
1923
DB_USER: fcrepo
2024
DB_PASSWORD: test-database-password
25+
jwt-keygen:
26+
<<: *common
27+
image: ${FCREPO7:-libops/fcrepo:local-7}
28+
entrypoint:
29+
- /bin/sh
30+
- -ec
31+
command:
32+
- |
33+
umask 077
34+
openssl genrsa -out /run/secrets/JWT_PRIVATE_KEY 2048
35+
openssl rsa \
36+
-in /run/secrets/JWT_PRIVATE_KEY \
37+
-pubout \
38+
-out /run/secrets/JWT_PUBLIC_KEY
39+
volumes:
40+
- jwt-keys:/run/secrets
2141
fcrepo:
2242
environment:
2343
DB_PASSWORD: test-database-password
2444
JWT_ADMIN_TOKEN: islandora
2545
volumes:
2646
- ./test.sh:/test.sh # Test to run.
47+
- jwt-keys:/run/secrets:ro
2748
command:
2849
- /test.sh # Run test and exit.
2950
image: ${FCREPO7:-libops/fcrepo:local-7}
@@ -32,3 +53,5 @@ services:
3253
condition: service_started
3354
mariadb:
3455
condition: service_healthy
56+
jwt-keygen:
57+
condition: service_completed_successfully

internal/buildkit/metadata_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,63 @@ func TestApplicationDatabaseBootstrapIsExplicit(t *testing.T) {
712712
}
713713
}
714714

715+
func TestBaseImageDoesNotShipJWTKeyMaterial(t *testing.T) {
716+
root := repoRoot(t)
717+
for _, name := range []string{"JWT_PRIVATE_KEY", "JWT_PUBLIC_KEY"} {
718+
path := filepath.Join(root, "images", "base", "rootfs", "etc", "defaults", name)
719+
content, err := os.ReadFile(path)
720+
if err != nil {
721+
t.Fatalf("read %s: %v", path, err)
722+
}
723+
if strings.TrimSpace(string(content)) != "" {
724+
t.Errorf("%s must not contain reusable JWT key material", path)
725+
}
726+
}
727+
}
728+
729+
func TestSynAuthenticationTestsGenerateEphemeralJWTKeys(t *testing.T) {
730+
root := repoRoot(t)
731+
for _, image := range []string{"fcrepo6", "fcrepo7"} {
732+
path := filepath.Join(root, "images", image, "tests", "SynAuthentication", "docker-compose.yml")
733+
content, err := os.ReadFile(path)
734+
if err != nil {
735+
t.Fatalf("read %s: %v", path, err)
736+
}
737+
738+
for _, required := range []string{
739+
"openssl genrsa -out /run/secrets/JWT_PRIVATE_KEY",
740+
"-out /run/secrets/JWT_PUBLIC_KEY",
741+
"jwt-keys:/run/secrets:ro",
742+
"condition: service_completed_successfully",
743+
} {
744+
if !strings.Contains(string(content), required) {
745+
t.Errorf("%s must contain %q", path, required)
746+
}
747+
}
748+
}
749+
}
750+
751+
func TestPublishedBaseImageRefreshesPackageMetadata(t *testing.T) {
752+
root := repoRoot(t)
753+
workflowPath := filepath.Join(root, ".github", "workflows", "build.yml")
754+
workflow, err := os.ReadFile(workflowPath)
755+
if err != nil {
756+
t.Fatalf("read %s: %v", workflowPath, err)
757+
}
758+
if !strings.Contains(string(workflow), `"BASE_NO_CACHE=${{ inputs.image == 'base' }}"`) {
759+
t.Fatalf("%s must disable the BuildKit cache for published base rebuilds", workflowPath)
760+
}
761+
762+
bakePath := filepath.Join(root, "docker-bake.hcl")
763+
bake, err := os.ReadFile(bakePath)
764+
if err != nil {
765+
t.Fatalf("read %s: %v", bakePath, err)
766+
}
767+
if !strings.Contains(string(bake), "no-cache = BASE_NO_CACHE") {
768+
t.Fatalf("%s must apply BASE_NO_CACHE to the base target", bakePath)
769+
}
770+
}
771+
715772
func TestMarketedApplicationImagesDoNotShipKnownCredentials(t *testing.T) {
716773
root := repoRoot(t)
717774
images := []string{"archivesspace", "drupal", "ojs", "omeka-s", "omeka-classic", "wp"}

0 commit comments

Comments
 (0)