Skip to content

Commit e8be350

Browse files
authored
Enable GPG checking when installing/updating packages. (#216)
Currently, GPG signature checking is disabled when installing/updating packages. This is less than ideal when downloading packages from external sources such as PMC (packages.microsoft.com). This change enables GPG checking by default for both the base image's repo files and the repo files provided by the user. However, the user can disable the GPG checks for their provided repo files by setting `gpgcheck` and `repo_gpgcheck` to `0`. GPG checking remains disabled for local directories since there is no way for the user to provide a GPG public key. Also, local directories are likely to contain a user's custom built packages anyway, which are unlikely to be signed.
1 parent 4c421f2 commit e8be350

8 files changed

Lines changed: 161 additions & 44 deletions

File tree

docs/imagecustomizer/api/cli.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,27 @@ Can be one of:
101101

102102
The RPMs may either be in the directory itself or any subdirectories.
103103

104+
GPG signature checking is disabled for local directories.
105+
If you wish to enable GPG signature checking, then use a repo file instead and set the
106+
`gpgkey` field within the repo file.
107+
104108
- `*.repo` file path: A path to a RPM repo definition file.
105109

106110
The file name extension must be `.repo`.
107111

108-
Note: This file is not installed in the image during customization.
109-
If that is also needed, then use `AdditionalFiles` to place the repo file within
112+
If the repo file's `baseurl` or `gpgkey` fields contain a `file://` URL, then the
113+
host's directories pointed to by the URL will be bind mounted into the chroot
114+
environment and the URL will be replaced with the chroot equivalent URL.
115+
116+
GPG signature checking is enabled by default.
117+
If you wish to disable GPG checking, then set both `gpgcheck` and `repo_gpgcheck` to
118+
`0` in the repo file.
119+
120+
The repo file will only be used during image customization and will not be added to
110121
the image.
122+
If you want to add the repo file to the image, then use use
123+
[additionalFiles](../api/configuration/os.md#additionalfiles-additionalfile) to place
124+
the repo file under the `/etc/yum.repos.d` directory.
111125

112126
This option can be specified multiple times.
113127

toolkit/tools/internal/safemount/safemount.go

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"fmt"
1010
"os"
11+
"path/filepath"
1112
"time"
1213

1314
"github.com/microsoft/azurelinux/toolkit/tools/internal/logger"
@@ -16,9 +17,9 @@ import (
1617
)
1718

1819
type Mount struct {
19-
target string
20-
isMounted bool
21-
dirCreated bool
20+
target string
21+
isMounted bool
22+
fileOrDirCreated bool
2223
}
2324

2425
// Creates a new system mount.
@@ -47,13 +48,37 @@ func (m *Mount) newMountHelper(source, target, fstype string, flags uintptr, dat
4748
source, target, fstype, flags, data)
4849

4950
if makeAndDeleteDir {
50-
// Create the mount target directory.
51-
err = os.MkdirAll(target, os.ModePerm)
51+
sourceInfo, err := os.Stat(source)
5252
if err != nil {
53-
return fmt.Errorf("failed to create mount directory (%s):\n%w", target, err)
53+
return fmt.Errorf("failed to stat mount source (%s):\n%w", source, err)
5454
}
5555

56-
m.dirCreated = true
56+
sourceIsFile := (sourceInfo.Mode() & os.ModeType) == 0
57+
isBindMount := (flags & unix.MS_BIND) == unix.MS_BIND
58+
if !sourceIsFile || !isBindMount {
59+
// Create the mount target directory.
60+
err = os.MkdirAll(target, os.ModePerm)
61+
if err != nil {
62+
return fmt.Errorf("failed to create mount directory (%s):\n%w", target, err)
63+
}
64+
} else {
65+
// Mount is a file bind mount.
66+
// So, create a placeholder file instead of a placeholder directory.
67+
68+
// Create parent directory.
69+
err = os.MkdirAll(filepath.Dir(target), os.ModePerm)
70+
if err != nil {
71+
return fmt.Errorf("failed to create mount target parent directory (%s):\n%w", target, err)
72+
}
73+
74+
// Create placeholder file.
75+
err = os.WriteFile(target, []byte(nil), os.ModePerm)
76+
if err != nil {
77+
return fmt.Errorf("failed to create mount target file (%s):\n%w", target, err)
78+
}
79+
}
80+
81+
m.fileOrDirCreated = true
5782
}
5883

5984
// Create the mount.
@@ -116,7 +141,7 @@ func (m *Mount) close(async bool) error {
116141
m.isMounted = false
117142
}
118143

119-
if m.dirCreated {
144+
if m.fileOrDirCreated {
120145
logger.Log.Debugf("Deleting directory (%s)", m.target)
121146

122147
// Note: Do not use `RemoveAll` here in case the unmount silently failed.
@@ -126,7 +151,7 @@ func (m *Mount) close(async bool) error {
126151
return fmt.Errorf("failed to delete mount directory (%s):\n%w", m.target, err)
127152
}
128153

129-
m.dirCreated = false
154+
m.fileOrDirCreated = false
130155
}
131156

132157
return nil

toolkit/tools/pkg/imagecustomizerlib/customizepackages.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func addRemoveAndUpdatePackages(buildDir string, baseConfigPath string, config *
106106

107107
func refreshTdnfMetadata(imageChroot *safechroot.Chroot) error {
108108
tdnfArgs := []string{
109-
"-v", "check-update", "--refresh", "--nogpgcheck", "--assumeyes",
109+
"-v", "check-update", "--refresh", "--assumeyes",
110110
"--setopt", fmt.Sprintf("reposdir=%s", rpmsMountParentDirInChroot),
111111
}
112112

@@ -168,7 +168,7 @@ func updateAllPackages(imageChroot *safechroot.Chroot) error {
168168
logger.Log.Infof("Updating base image packages")
169169

170170
tdnfUpdateArgs := []string{
171-
"-v", "update", "--nogpgcheck", "--assumeyes", "--cacheonly",
171+
"-v", "update", "--assumeyes", "--cacheonly",
172172
"--setopt", fmt.Sprintf("reposdir=%s", rpmsMountParentDirInChroot),
173173
}
174174

@@ -189,7 +189,7 @@ func installOrUpdatePackages(action string, allPackagesToAdd []string, imageChro
189189
// Note: When using `--repofromdir`, tdnf will not use any default repos and will only use the last
190190
// `--repofromdir` specified.
191191
tdnfInstallArgs := []string{
192-
"-v", action, "--nogpgcheck", "--assumeyes", "--cacheonly",
192+
"-v", action, "--assumeyes", "--cacheonly",
193193
"--setopt", fmt.Sprintf("reposdir=%s", rpmsMountParentDirInChroot),
194194
}
195195

toolkit/tools/pkg/imagecustomizerlib/customizepackages_test.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,22 @@ func copyRpms(sourceDir string, targetDir string, excludePrefixes []string) erro
132132
return nil
133133
}
134134

135-
func TestCustomizeImagePackagesAddOfflineLocalRepo(t *testing.T) {
136-
testTmpDir := filepath.Join(tmpDir, "TestCustomizeImagePackagesAddOfflineLocalRepo")
135+
func TestCustomizeImagePackagesAddOfflineLocalRepoWithGpgKey(t *testing.T) {
136+
testCustomizeImagePackagesAddOfflineLocalRepoHelper(t, "TestCustomizeImagePackagesAddOfflineLocalRepoWithGpgKey",
137+
true)
138+
}
139+
140+
func TestCustomizeImagePackagesAddOfflineLocalRepoNoGpgKey(t *testing.T) {
141+
testCustomizeImagePackagesAddOfflineLocalRepoHelper(t, "TestCustomizeImagePackagesAddOfflineLocalRepoNoGpgKey",
142+
false)
143+
}
144+
145+
func testCustomizeImagePackagesAddOfflineLocalRepoHelper(t *testing.T, testName string, withGpgKey bool) {
146+
testTmpDir := filepath.Join(tmpDir, testName)
137147

138148
baseImage := checkSkipForCustomizeImage(t, baseImageTypeCoreEfi, baseImageVersionDefault)
139149

140-
downloadedRpmsRepoFile := getDownloadedRpmsRepoFile(t, "2.0")
150+
downloadedRpmsRepoFile := getDownloadedRpmsRepoFile(t, "2.0", withGpgKey)
141151
rpmSources := []string{downloadedRpmsRepoFile}
142152

143153
buildDir := filepath.Join(testTmpDir, "build")

toolkit/tools/pkg/imagecustomizerlib/main_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,14 @@ func getDownloadedRpmsDir(t *testing.T, azureLinuxVersion string) string {
140140
return downloadedRpmsDir
141141
}
142142

143-
func getDownloadedRpmsRepoFile(t *testing.T, azureLinuxVersion string) string {
143+
func getDownloadedRpmsRepoFile(t *testing.T, azureLinuxVersion string, withGpgKey bool) string {
144144
dir := getDownloadedRpmsDir(t, azureLinuxVersion)
145-
repoFile := filepath.Join(dir, "../", fmt.Sprintf("rpms-%s.repo", azureLinuxVersion))
145+
146+
suffix := "nokey"
147+
if withGpgKey {
148+
suffix = "withkey"
149+
}
150+
151+
repoFile := filepath.Join(dir, "../", fmt.Sprintf("rpms-%s-%s.repo", azureLinuxVersion, suffix))
146152
return repoFile
147153
}

toolkit/tools/pkg/imagecustomizerlib/rpmsourcesmounts.go

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,13 @@ func (m *rpmSourcesMounts) createRepoFromDirectory(rpmSource string, allReposCon
147147
rpmSourceName := path.Base(rpmSource)
148148

149149
// Mount the directory.
150-
mountTargetDirectoryInChroot, err := m.mountRpmsDirectory(rpmSourceName, rpmSource, imageChroot)
150+
mountTargetDirectoryInChroot, err := m.mountRpmsSource("baseurl", rpmSourceName, rpmSource, imageChroot)
151151
if err != nil {
152152
return err
153153
}
154154

155155
// Add local repo config.
156-
err = appendLocalRepo(allReposConfig, mountTargetDirectoryInChroot)
156+
err = appendLocalRepo(allReposConfig, mountTargetDirectoryInChroot, rpmSource)
157157
if err != nil {
158158
return fmt.Errorf("failed to append local repo config:\n%w", err)
159159
}
@@ -181,25 +181,52 @@ func (m *rpmSourcesMounts) createRepoFromRepoConfig(rpmSource string, isHostConf
181181
}
182182

183183
if isHostConfig {
184-
baseUrlKey, err := repoConfig.GetKey("baseurl")
184+
_, err = repoConfig.GetKey("baseurl")
185185
if err != nil {
186186
return fmt.Errorf("invalid repo config (%s):\n%w", rpmSource, err)
187187
}
188188

189-
// Check if the repo points to a local directory.
190-
baseurl := baseUrlKey.String()
191-
filePath, hasFilePrefix := strings.CutPrefix(baseurl, "file://")
192-
if hasFilePrefix {
193-
// Mount the directory in the chroot.
194-
rpmSourceName := path.Base(baseurl)
195-
mountTargetDirectoryInChroot, err := m.mountRpmsDirectory(rpmSourceName, filePath, imageChroot)
189+
gpgCheckKey, _ := repoConfig.GetKey("gpgcheck")
190+
repoGpgCheckKey, _ := repoConfig.GetKey("repo_gpgcheck")
191+
switch {
192+
case gpgCheckKey != nil && gpgCheckKey.String() != "1":
193+
logger.Log.Infof("GPG signature checking disabled for RPM repo (%s)", repoConfig.Name())
194+
195+
case repoGpgCheckKey != nil && repoGpgCheckKey.String() != "1":
196+
logger.Log.Infof("GPG signature checking disabled for RPM repo metadata (%s)", repoConfig.Name())
197+
}
198+
199+
for _, field := range []string{"baseurl", "gpgkey"} {
200+
fieldKey, err := repoConfig.GetKey(field)
196201
if err != nil {
197-
return fmt.Errorf("failed mount repo config local directory (%s):\n%w", rpmSource, err)
202+
// Field doesn't exist.
203+
continue
198204
}
199205

200-
// Change the baseurl to point to the bind mount directory.
201-
newBaseurl := fmt.Sprintf("file://%s", mountTargetDirectoryInChroot)
202-
repoConfig.Key("baseurl").SetValue(newBaseurl)
206+
fieldValue := fieldKey.String()
207+
values := strings.Fields(fieldValue)
208+
209+
newValues := []string(nil)
210+
for _, value := range values {
211+
// Check if the value points to a local file/directory.
212+
filePath, hasFilePrefix := strings.CutPrefix(value, "file://")
213+
if hasFilePrefix {
214+
// Bind mount the file/directory in the chroot.
215+
rpmSourceName := path.Base(value)
216+
mountTargetDirectoryInChroot, err := m.mountRpmsSource(field, rpmSourceName, filePath,
217+
imageChroot)
218+
if err != nil {
219+
return fmt.Errorf("failed mount repo config local file/directory (%s):\n%w", filePath, err)
220+
}
221+
222+
// Change the value to point to the bind mount file/directory.
223+
newValue := fmt.Sprintf("file://%s", mountTargetDirectoryInChroot)
224+
newValues = append(newValues, newValue)
225+
}
226+
}
227+
228+
newFieldValue := strings.Join(newValues, " ")
229+
repoConfig.Key(field).SetValue(newFieldValue)
203230
}
204231
}
205232

@@ -213,18 +240,18 @@ func (m *rpmSourcesMounts) createRepoFromRepoConfig(rpmSource string, isHostConf
213240
return nil
214241
}
215242

216-
func (m *rpmSourcesMounts) mountRpmsDirectory(rpmSourceName string, rpmsDirectory string,
243+
func (m *rpmSourcesMounts) mountRpmsSource(fieldName string, sourceName string, sourcePath string,
217244
imageChroot *safechroot.Chroot,
218245
) (string, error) {
219246
i := len(m.mounts)
220-
targetName := fmt.Sprintf("%02d%s", i, rpmSourceName)
247+
targetName := fmt.Sprintf("%02d-%s-%s", i, fieldName, sourceName)
221248
mountTargetDirectoryInChroot := path.Join(rpmsMountParentDirInChroot, targetName)
222249
mountTargetDirectory := path.Join(imageChroot.RootDir(), mountTargetDirectoryInChroot)
223250

224251
// Create a read-only bind mount.
225-
mount, err := safemount.NewMount(rpmsDirectory, mountTargetDirectory, "", unix.MS_BIND|unix.MS_RDONLY, "", true)
252+
mount, err := safemount.NewMount(sourcePath, mountTargetDirectory, "", unix.MS_BIND|unix.MS_RDONLY, "", true)
226253
if err != nil {
227-
return "", fmt.Errorf("failed to mount RPM source directory from (%s) to (%s):\n%w", rpmsDirectory, mountTargetDirectory, err)
254+
return "", fmt.Errorf("failed to mount RPM source from (%s) to (%s):\n%w", sourcePath, mountTargetDirectory, err)
228255
}
229256

230257
m.mounts = append(m.mounts, mount)
@@ -310,7 +337,7 @@ func getRpmSourceFileType(rpmSourcePath string) (string, error) {
310337
}
311338

312339
// Add a local directory containing RPMs to the allrepos.repo file.
313-
func appendLocalRepo(iniFile *ini.File, mountTargetDirectoryInChroot string) error {
340+
func appendLocalRepo(iniFile *ini.File, mountTargetDirectoryInChroot string, rpmSource string) error {
314341
repoName := filepath.Base(mountTargetDirectoryInChroot)
315342
iniSection, err := iniFile.NewSection(repoName)
316343
if err != nil {
@@ -334,6 +361,22 @@ func appendLocalRepo(iniFile *ini.File, mountTargetDirectoryInChroot string) err
334361
return err
335362
}
336363

364+
// Disable GPG checks for local directories.
365+
// There is no API to specify the GPG public key to use to verify the packages in the local directories.
366+
// Also, local directories are likely to contain a user's custom built packages, which are very unlikely to be
367+
// signed. If a user does sign their own packages, then they can pass in a .repo file instead and set the 'gpgkey'
368+
// field within the .repo file.
369+
_, err = iniSection.NewKey("gpgcheck", "0")
370+
if err != nil {
371+
return err
372+
}
373+
374+
_, err = iniSection.NewKey("repo_gpgcheck", "0")
375+
if err != nil {
376+
return err
377+
}
378+
379+
logger.Log.Infof("GPG signature checking disabled for RPM repo (%s)", rpmSource)
337380
return nil
338381
}
339382

toolkit/tools/pkg/imagecustomizerlib/testdata/testrpms/download-test-rpms.sh

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ set -x
3737

3838
DOWNLOADER_RPMS_DIRS="$SCRIPT_DIR/downloadedrpms"
3939
OUT_DIR="$DOWNLOADER_RPMS_DIRS/$IMAGE_VERSION"
40-
REPO_FILE="$DOWNLOADER_RPMS_DIRS/rpms-$IMAGE_VERSION.repo"
40+
REPO_WITH_KEY_FILE="$DOWNLOADER_RPMS_DIRS/rpms-$IMAGE_VERSION-withkey.repo"
41+
REPO_NO_KEY_FILE="$DOWNLOADER_RPMS_DIRS/rpms-$IMAGE_VERSION-nokey.repo"
4142

4243
mkdir -p "$OUT_DIR"
4344

@@ -50,14 +51,32 @@ docker build \
5051
# Extract the RPM files.
5152
docker run \
5253
--rm \
53-
-v $OUT_DIR:/outdir:z \
54+
-v $OUT_DIR:/rpmsdir:z \
5455
"$CONTAINER_TAG" \
55-
cp -r /downloadedrpms/. "/outdir"
56+
cp -r /downloadedrpms/. "/rpmsdir"
5657

57-
# Create repo file.
58-
cat << EOF > "$REPO_FILE"
58+
docker run \
59+
--rm \
60+
-v $OUT_DIR:/rpmsdir:z \
61+
"$CONTAINER_TAG" \
62+
cp -r /etc/pki/rpm-gpg/. "/rpmsdir"
63+
64+
# Create repo files.
65+
cat << EOF > "$REPO_WITH_KEY_FILE"
66+
[localrpms]
67+
name=Local RPMs repo
68+
baseurl=file://$OUT_DIR
69+
enabled=1
70+
gpgcheck=1
71+
repo_gpgcheck=0
72+
gpgkey=file://$OUT_DIR/MICROSOFT-RPM-GPG-KEY
73+
EOF
74+
75+
cat << EOF > "$REPO_NO_KEY_FILE"
5976
[localrpms]
6077
name=Local RPMs repo
6178
baseurl=file://$OUT_DIR
6279
enabled=1
80+
gpgcheck=0
81+
repo_gpgcheck=0
6382
EOF

toolkit/tools/pkg/imagecustomizerlib/testdata/testrpms/downloader/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ RUN tdnf update -y && \
55
tdnf install -y dnf dnf-plugins-core createrepo_c
66

77
# Download the RPMs needed by the following tests:
8-
# - TestCustomizeImageAddPackage
8+
# - TestCustomizeImagePackagesAddOfflineLocalRepo
99
RUN dnf download -y --resolve --alldeps --destdir /downloadedrpms \
1010
jq \
1111
golang

0 commit comments

Comments
 (0)