-
Notifications
You must be signed in to change notification settings - Fork 50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
osbuild: new stage 'cacert' #907
base: main
Are you sure you want to change the base?
Conversation
pkg/manifest/os.go
Outdated
if len(p.CACerts) > 0 { | ||
for _, cc := range p.CACerts { | ||
for _, c := range parseCerts(cc) { | ||
path := filepath.Join("/etc/pki/ca-trust/source/anchors", filepath.Base(c.SerialNumber.Text(16))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing the filename extension, will add during review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we break down this PR into multiple commits for easier reviewing and history?
I like to have commits that introduce new things separate from the following ones that use them, so maybe:
- Add
osbuild/..._stage.go
- Add
manifest/os.go
changes. - Add blueprint customization.
- Add config option to
all-customizations.json
.
(though, 2 and 3 can be merged).
pkg/blueprint/customizations.go
Outdated
@@ -32,6 +32,7 @@ type Customizations struct { | |||
Installer *InstallerCustomization `json:"installer,omitempty" toml:"installer,omitempty"` | |||
RPM *RPMCustomization `json:"rpm,omitempty" toml:"rpm,omitempty"` | |||
RHSM *RHSMCustomization `json:"rhsm,omitempty" toml:"rhsm,omitempty"` | |||
CA *CACustomization `json:"ca,omitempty" toml:"ca,omitempty"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ca
, while an accurate initialism and probably a very common term to refer to what we're doing, is probably not a great name for a configuration key in the soup of top-level blueprint customization names.
CertificateAuthority
(certificate-authority
) is, on the other hand, too long.
Maybe CACerts
(toml:"ca-certs"
) would be a better name?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, in the same struct I see a two-worded JSON field SSHKey as sshkey
so I will stick with this theme and go for cacerts
.
test/configs/all-customizations.json
Outdated
"ca": [ | ||
"-----BEGIN CERTIFICATE-----\nMIIDszCCApugAwIBAgIUJ4lK+JfdJCNgcEVxZDinJfKKbQswDQYJKoZIhvcNAQEL\nBQAwaDELMAkGA1UEBhMCVVMxFzAVBgNVBAgMDk5vcnRoIENhcm9saW5hMRAwDgYD\nVQQHDAdSYWxlaWdoMRAwDgYDVQQKDAdSZWQgSGF0MRwwGgYDVQQDDBNUZXN0IENB\nIGZvciBvc2J1aWxkMCAXDTI0MDkwMzEzMjkyMFoYDzIyOTgwNjE4MTMyOTIwWjBo\nMQswCQYDVQQGEwJVUzEXMBUGA1UECAwOTm9ydGggQ2Fyb2xpbmExEDAOBgNVBAcM\nB1JhbGVpZ2gxEDAOBgNVBAoMB1JlZCBIYXQxHDAaBgNVBAMME1Rlc3QgQ0EgZm9y\nIG9zYnVpbGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDeA7OcWTrV\ngstoBsUaeJKm8nelg7Lc0WNXH6yOTLsr4td4yHs0YOvFGwgSf+ffV3RAG1mgqnMG\nMgkD2+z+7QhHbHHs3y0d0zfhA2bg0KVvfCWk7fNRPHY0UOePpXk245Bfw3D0VTpl\nF7nePk1I7ZY09snPWUeb2rjKXzYjKjzM0h27+ykV8I8+FbdyPk/pR8whyDqtHLUa\nXfFy2TFloDSYMkHKVd38BnL0bj91x5F+KsZkN4HzfbYwxLbCQfOSgy7q6TWce9kq\nLo6tya9vuvpWFm1dye7L+BodAQAq/dI/JMeCfyTb0eFb+tyzfr5aVIoqqDN+p9ft\ncw4OefpHbhtNAgMBAAGjUzBRMB0GA1UdDgQWBBRV2A9YmusekPzu5Yf08cV0oPL1\nwjAfBgNVHSMEGDAWgBRV2A9YmusekPzu5Yf08cV0oPL1wjAPBgNVHRMBAf8EBTAD\nAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCgQZ2Xfj+NxaKBZgn2KNxS0MTbhzHRz6Rn\nqJs+h8OUz2Crmaf6N+RHlmDRZXUrDjSHpxVT2LxFy7ofRrLYIezFDUYfb920VkkV\nSVcxh1YDFROJalfMoE6wdyR/LnK4MJZS9fUpeCJJc/A0J+9FK9CwcyUrHgJ8XbJh\nMKYyQ+cf6O7wzutuBpMyRqSKS+hVM7BQTmSFvv1eAJlo6klGAmmKiYmAEvcQadH1\ndjrujsA3Cn5vX2L+0yuiLB5/zoxqx5cEy97TuKUYB8OqMMujAXNzF4L3HJDUNba2\nAhEkFozMXwYX73TGbGZ0mawPS5D3v3tYTEmJFf6SnVCmUW1fs57g\n-----END CERTIFICATE-----\n" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't match the customization struct in the code. The way the customization is written now this should be:
"ca": {
"certs": [
"..."
]
}
pkg/osbuild/cacert_stage.go
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The stage is called pki.update-ca-trust
so please name the file accordingly for easier navigation.
pki_update_stage.go
for example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we break down this PR into multiple commits for easier reviewing and history?
I like to have commits that introduce new things separate from the following ones that use them, so maybe:
- Add
osbuild/..._stage.go
- Add
manifest/os.go
changes. - Add blueprint customization.
- Add config option to
all-customizations.json
.
(though, 2 and 3 can be merged).
Sure, amended all remarks. If you don’t mind, I am keeping one commit for easier work from my side, multiple commits rebasing is a pain to work with this isn’t a big PR by any means. I will split it after the code is ready. |
This PR is stale because it has been open 30 days with no activity. Remove "Stale" label or comment or this will be closed in 7 days. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this PR! It looks good but I still have some ideas/suggestions inline for your consideration. But I'm happy to help with them if you have no enough time :)
pkg/manifest/os.go
Outdated
for _, block := range blocks { | ||
cert, err := x509.ParseCertificate(block) | ||
if err != nil { | ||
logrus.Warnf("failed to parse certificate, skipping: %s", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the behavior we want/expect? This will mean that pasing an invalid cert string will just result in an empty list and the cert not appearing in the manifest. As a user I would probably be surprised if I paste a cert with an error and then get an image (much later) that does not contain it. Sorry, I think this is a very wordy way of saysing: "it seems to me we should return an error here" (unless I miss something of course which is entriely possible)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call, explicit error it is.
pkg/manifest/os.go
Outdated
@@ -22,6 +24,7 @@ import ( | |||
"github.com/osbuild/images/pkg/platform" | |||
"github.com/osbuild/images/pkg/rhsm/facts" | |||
"github.com/osbuild/images/pkg/rpmmd" | |||
"github.com/sirupsen/logrus" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(nipick) in general our convention for imports is (in most go project btw):
- stdlib
- external third party
- our own includes
we are not very strict about it but here I would suggest to move it between "strings" and github.com/osbuild/images/internal/common" (with \n before/after)
|
||
func (c *Customizations) GetCACerts() *CACustomization { | ||
if c == nil { | ||
return nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if it wouldn't be better to also validate here, this would avoid the panic for invalid certs later. Here we could follow the pattern from e.g. GetRepositories() and return (*CACustomization,error) and then the callers (in fedora for example) can report a meaningful error for invalid cert strings early. Wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can do that, just note this introduces a dependency on distro/manifest package. Also validating on a getter is something that can possibly cause performance issues later. Finally, getters are generally not recommended in Go communty.
Overall, I do not like this, but can do that we can discuss it over the code and I can revert the hunk if needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah it is an import cycle actually, I cannot do that, do you want me to do it? I would suggest to create an explicit CheckCACerts
function and extract the parsing X509 code into a separate package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for looking into this, I'm not sure I follow, maybe I'm missing something but I was thinking of something like this:
diff --git a/pkg/blueprint/customizations.go b/pkg/blueprint/customizations.go
index 0bb5bacbe..f0d800077 100644
--- a/pkg/blueprint/customizations.go
+++ b/pkg/blueprint/customizations.go
@@ -428,12 +428,12 @@ func (c *Customizations) GetRHSM() *RHSMCustomization {
return c.RHSM
}
-func (c *Customizations) CheckCACerts() error {
+func (c *Customizations) checkCACerts() error {
if c == nil {
return nil
}
- if c.CACerts == nil {
+ if c.CACerts != nil {
for _, bundle := range c.CACerts.PEMCerts {
_, err := cert.ParseCerts(bundle)
if err != nil {
@@ -445,9 +445,14 @@ func (c *Customizations) CheckCACerts() error {
return nil
}
-func (c *Customizations) GetCACerts() *CACustomization {
+func (c *Customizations) GetCACerts() (*CACustomization, error) {
if c == nil {
- return nil
+ return nil, nil
}
- return c.CACerts
+
+ if err := c.checkCACerts(); err != nil {
+ return nil, err
+ }
+
+ return c.CACerts, nil
}
diff --git a/pkg/distro/fedora/images.go b/pkg/distro/fedora/images.go
index b0e737468..4c04a06c4 100644
--- a/pkg/distro/fedora/images.go
+++ b/pkg/distro/fedora/images.go
@@ -226,10 +226,11 @@ func osCustomizations(
osc.Files = append(osc.Files, imageConfig.Files...)
osc.Directories = append(osc.Directories, imageConfig.Directories...)
- if err := c.CheckCACerts(); err != nil {
+ ca, err := c.GetCACerts()
+ if err != nil {
panic(fmt.Sprintf("unexpected error checking CA certs: %v", err))
}
- if ca := c.GetCACerts(); ca != nil {
+ if ca != nil {
osc.CACerts = ca.PEMCerts
}
diff --git a/pkg/distro/rhel/images.go b/pkg/distro/rhel/images.go
index 5e13afe7f..806463b92 100644
--- a/pkg/distro/rhel/images.go
+++ b/pkg/distro/rhel/images.go
@@ -271,10 +271,11 @@ func osCustomizations(
osc.NoBLS = *imageConfig.NoBLS
}
- if err := c.CheckCACerts(); err != nil {
+ ca, err := c.GetCACerts()
+ if err != nil {
panic(fmt.Sprintf("unexpected error checking CA certs: %v", err))
}
- if ca := c.GetCACerts(); ca != nil {
+ if ca != nil {
osc.CACerts = ca.PEMCerts
}
i.e. every time we call GetCACerts() we implicitly check as well - this way we will not forget and it's similar to the pattern of GetRepositories() ?
pkg/manifest/os.go
Outdated
return pipeline | ||
} | ||
|
||
func parseCerts(cert string) []*x509.Certificate { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A simple unit test for this would be very nice, ideally both for a "positive" and "error" case, something like
// taken from osbuild:test/data/certs/cert1.pem
const exampleCert = `
-----BEGIN CERTIFICATE-----
MIIDhTCCAm2gAwIBAgIUVya7VJ3O8W8SqwuEa0BZ4HSsXvAwDQYJKoZIhvcNAQEL
BQAwUTELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVy
bGluMQwwCgYDVQQKDANPcmcxEjAQBgNVBAMMCWxvY2FsaG9zdDAgFw0yNDA4MjYx
MDQyNDBaGA8yMTI0MDgwMjEwNDI0MFowUTELMAkGA1UEBhMCREUxDzANBgNVBAgM
BkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMQwwCgYDVQQKDANPcmcxEjAQBgNVBAMM
CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnGjlvN
O3F/Z7Lr/r+6Xp2DosnNwoPHhG2e61KnFzgZfaxbklal5ORpuV/gLIg7lrbpdZe7
WvK+16RanL6fLitis/tYVFyvz1MXqBYYrEoFGvVg9fOiis7hjpdZcpNDH9SngoAN
O0Wvv4T6LQS0cC7ZAFZjvmJ+RiZEbzRkNG5pUddZXbotE6htNfLgA5L1wIBgllrM
4DVkG0yNKmzqPNzfPTbdUgWCfjaQShHy1GP8KNEwFxM31F2wvQxsEb77o1S44Out
mlsi83tti6P7KjDk7w2j2zZO1X0xI8pflv3TBkJT1Am8vnk6rVnNO4pCpop3+kma
pDUEzBQmSQA5R1ECAwEAAaNTMFEwHQYDVR0OBBYEFDxFcFgPEsgsDixfKxB0uYGN
aJmzMB8GA1UdIwQYMBaAFDxFcFgPEsgsDixfKxB0uYGNaJmzMA8GA1UdEwEB/wQF
MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFih4lUbLlhKwIAV9x3/W7Mih8xUEdZr
olquZgaHedFet+ByAHvoES3pec7AVYTOD53mjgyZubD6INnVHzKyS4AG9ydD73o4
cmm3DKxBaesvlHeTn0MOKsoM8QCxeyFJmiUPpgDBok/PFnbGR9+JcsrlGJAnsSKD
vWpiwYcBauZ9nnK5yDe5M9XNFPkNDZzbKvWU7Sw3ziMT/+bRJse5vTrYcyOnNGgy
gZNz2nimKy1U8XZVAVwOV0rdGEFrfMln8DkRW86rGK/EncaVsl0SSP/rmjQgiX8Q
3CZraQGujJP932HSwUfdCX9yh+rTjE3MEnbqMoLzJa4BXB2aDQWtywU=
-----END CERTIFICATE-----
`
func TestParseCerts(t *testing.T) {
certs := parseCerts(exampleCert)
assert.Equal(t, len(certs), 1)
assert.Equal(t, "localhost", certs[0].Subject.CommonName)
}
maybe?
I have extracted the checking code + test into separate package |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the update, looks very nice now, I made a suggestion inline, hope it's useful.
return nil | ||
} | ||
|
||
if c.CACerts == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be !=
, no? If so ideally that would have triggered a test failure :/
|
||
func (c *Customizations) GetCACerts() *CACustomization { | ||
if c == nil { | ||
return nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for looking into this, I'm not sure I follow, maybe I'm missing something but I was thinking of something like this:
diff --git a/pkg/blueprint/customizations.go b/pkg/blueprint/customizations.go
index 0bb5bacbe..f0d800077 100644
--- a/pkg/blueprint/customizations.go
+++ b/pkg/blueprint/customizations.go
@@ -428,12 +428,12 @@ func (c *Customizations) GetRHSM() *RHSMCustomization {
return c.RHSM
}
-func (c *Customizations) CheckCACerts() error {
+func (c *Customizations) checkCACerts() error {
if c == nil {
return nil
}
- if c.CACerts == nil {
+ if c.CACerts != nil {
for _, bundle := range c.CACerts.PEMCerts {
_, err := cert.ParseCerts(bundle)
if err != nil {
@@ -445,9 +445,14 @@ func (c *Customizations) CheckCACerts() error {
return nil
}
-func (c *Customizations) GetCACerts() *CACustomization {
+func (c *Customizations) GetCACerts() (*CACustomization, error) {
if c == nil {
- return nil
+ return nil, nil
}
- return c.CACerts
+
+ if err := c.checkCACerts(); err != nil {
+ return nil, err
+ }
+
+ return c.CACerts, nil
}
diff --git a/pkg/distro/fedora/images.go b/pkg/distro/fedora/images.go
index b0e737468..4c04a06c4 100644
--- a/pkg/distro/fedora/images.go
+++ b/pkg/distro/fedora/images.go
@@ -226,10 +226,11 @@ func osCustomizations(
osc.Files = append(osc.Files, imageConfig.Files...)
osc.Directories = append(osc.Directories, imageConfig.Directories...)
- if err := c.CheckCACerts(); err != nil {
+ ca, err := c.GetCACerts()
+ if err != nil {
panic(fmt.Sprintf("unexpected error checking CA certs: %v", err))
}
- if ca := c.GetCACerts(); ca != nil {
+ if ca != nil {
osc.CACerts = ca.PEMCerts
}
diff --git a/pkg/distro/rhel/images.go b/pkg/distro/rhel/images.go
index 5e13afe7f..806463b92 100644
--- a/pkg/distro/rhel/images.go
+++ b/pkg/distro/rhel/images.go
@@ -271,10 +271,11 @@ func osCustomizations(
osc.NoBLS = *imageConfig.NoBLS
}
- if err := c.CheckCACerts(); err != nil {
+ ca, err := c.GetCACerts()
+ if err != nil {
panic(fmt.Sprintf("unexpected error checking CA certs: %v", err))
}
- if ca := c.GetCACerts(); ca != nil {
+ if ca != nil {
osc.CACerts = ca.PEMCerts
}
i.e. every time we call GetCACerts() we implicitly check as well - this way we will not forget and it's similar to the pattern of GetRepositories() ?
@@ -822,6 +826,25 @@ func (p *OS) serialize() osbuild.Pipeline { | |||
} | |||
} | |||
|
|||
if len(p.CACerts) > 0 { | |||
for _, cc := range p.CACerts { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would love to see a test for this but I understand this area is really difficult to test so fine to ignore that (but I also can't help to mention it)
This pull request adds a new stage called 'cacert' to the osbuild package. It also includes file changes to support the CACustomization feature, which allows users to specify a list of certificates for the CA (Certificate Authority). The changes include updates to the Customizations struct, the osCustomizations function, and the OSCustomizations struct. Additionally, a new function called parseCerts is added to parse the certificate strings. The changes also include updates to the serialize and prependKernelCmdlineStage functions in the OS struct. Finally, a new file called ca_stage.go is added to the osbuild package, which contains the implementation of the NewCAStageStage function.
Needs: osbuild/osbuild#1854