diff --git a/config/v3_4_experimental/schema/ignition.json b/config/v3_4_experimental/schema/ignition.json index 03fe244832..bac8fcb9be 100644 --- a/config/v3_4_experimental/schema/ignition.json +++ b/config/v3_4_experimental/schema/ignition.json @@ -328,6 +328,9 @@ }, "thumbprint": { "type": ["string", "null"] + }, + "advertisement": { + "type": ["string","null"] } } }, diff --git a/config/v3_4_experimental/translate/translate.go b/config/v3_4_experimental/translate/translate.go index 2b00510c71..b9ef6a6860 100644 --- a/config/v3_4_experimental/translate/translate.go +++ b/config/v3_4_experimental/translate/translate.go @@ -55,6 +55,7 @@ func translateDirectoryEmbedded1(old old_types.DirectoryEmbedded1) (ret types.Di func translateLuks(old old_types.Luks) (ret types.Luks) { tr := translate.NewTranslator() + tr.AddCustomTranslator(translateTang) tr.Translate(&old.Clevis, &ret.Clevis) tr.Translate(&old.Device, &ret.Device) tr.Translate(&old.KeyFile, &ret.KeyFile) @@ -66,6 +67,13 @@ func translateLuks(old old_types.Luks) (ret types.Luks) { return } +func translateTang(old old_types.Tang) (ret types.Tang) { + tr := translate.NewTranslator() + tr.Translate(&old.Thumbprint, &ret.Thumbprint) + tr.Translate(&old.URL, &ret.URL) + return +} + func Translate(old old_types.Config) (ret types.Config) { tr := translate.NewTranslator() tr.AddCustomTranslator(translateIgnition) diff --git a/config/v3_4_experimental/types/schema.go b/config/v3_4_experimental/types/schema.go index 768b7d26ef..7c719361a2 100644 --- a/config/v3_4_experimental/types/schema.go +++ b/config/v3_4_experimental/types/schema.go @@ -236,8 +236,9 @@ type TLS struct { } type Tang struct { - Thumbprint *string `json:"thumbprint,omitempty"` - URL string `json:"url,omitempty"` + Thumbprint *string `json:"thumbprint,omitempty"` + URL string `json:"url,omitempty"` + Advertisement *string `json:"advertisement,omitempty"` } type Timeouts struct { diff --git a/docs/configuration-v3_4_experimental.md b/docs/configuration-v3_4_experimental.md index 503803b36f..2742a0d216 100644 --- a/docs/configuration-v3_4_experimental.md +++ b/docs/configuration-v3_4_experimental.md @@ -143,6 +143,7 @@ The Ignition configuration is a JSON document conforming to the following specif * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. * **url** (string): url of the tang server. * **thumbprint** (string): thumbprint of a trusted signing key. + * **advertisement** (string): A trusted advertisement json from a Tang server for initial provisioning. When supplied, the advertisement is used in place of fetching one from the Tang server on first boot. Thus enabling offline provisioning. * **_tpm2_** (bool): whether or not to use a tpm2 device. * **_threshold_** (int): sets the minimum number of pieces required to decrypt the device. Default is 1. * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. diff --git a/docs/release-notes.md b/docs/release-notes.md index 50e102df39..3bbe0fbb53 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -14,6 +14,7 @@ nav_order: 9 - Ship aarch64 macOS ignition-validate binary in GitHub release artifacts - Allow enabling discard passthrough on LUKS devices _(3.4.0-exp)_ - Allow specifying arbitrary LUKS open options _(3.4.0-exp)_ +- Support offline Tang provisioning via pre-shared advertisement _(3.4.0-exp)_ ### Changes diff --git a/internal/exec/stages/disks/luks.go b/internal/exec/stages/disks/luks.go index 192fbcb1e8..9ded9d7ac5 100644 --- a/internal/exec/stages/disks/luks.go +++ b/internal/exec/stages/disks/luks.go @@ -41,8 +41,9 @@ var ( // https://github.com/latchset/clevis/blob/master/src/pins/tang/clevis-encrypt-tang.1.adoc#config type Tang struct { - URL string `json:"url"` - Thumbprint string `json:"thp,omitempty"` + URL string `json:"url"` + Thumbprint string `json:"thp,omitempty"` + Advertisement string `json:"adv,omitempty"` } // https://github.com/latchset/clevis/blob/master/README.md#pin-shamir-secret-sharing @@ -273,10 +274,19 @@ func (s *stage) createLuks(config types.Config) error { c.Threshold = *luks.Clevis.Threshold } for _, tang := range luks.Clevis.Tang { - c.Pins.Tang = append(c.Pins.Tang, Tang{ - URL: tang.URL, - Thumbprint: *tang.Thumbprint, - }) + if !util.NilOrEmpty(tang.Advertisement) { + c.Pins.Tang = append(c.Pins.Tang, Tang{ + URL: tang.URL, + Thumbprint: *tang.Thumbprint, + Advertisement: *tang.Advertisement, + }) + } else { + + c.Pins.Tang = append(c.Pins.Tang, Tang{ + URL: tang.URL, + Thumbprint: *tang.Thumbprint, + }) + } } if luks.Clevis.Tpm2 != nil { c.Pins.Tpm = *luks.Clevis.Tpm2 @@ -294,7 +304,21 @@ func (s *stage) createLuks(config types.Config) error { // pass the device to clevis. We have to loop each device as // the devices could be on different NICs that haven't come // up yet. + //create two pools of tang server's online only and offline first boot. + tangServersWithAdvertisement := []types.Tang{} + tangServersWithoutAdvertisement := []types.Tang{} + for _, tang := range luks.Clevis.Tang { + // If the advertisement is already set, it means its offline first boot + if util.NotEmpty(tang.Advertisement) { + tangServersWithAdvertisement = append(tangServersWithAdvertisement, tang) + } else { + tangServersWithoutAdvertisement = append(tangServersWithoutAdvertisement, tang) + } + } + + // first lets see if we can reach the tang servers without advertisement + for _, tang := range tangServersWithoutAdvertisement { u, err := url.Parse(tang.URL) if err != nil { return fmt.Errorf("parsing tang URL: %v", err) @@ -306,24 +330,52 @@ func (s *stage) createLuks(config types.Config) error { } } + // additionally if there are tang servers with advertisement, lets see if they are reachable, and check to make sure the keys match + for _, tang := range tangServersWithAdvertisement { + u, err := url.Parse(tang.URL) + if err != nil { + return fmt.Errorf("parsing tang URL: %v", err) + } + u.Path = path.Join(u.Path, "adv") + data, err := s.Fetcher.FetchToBuffer(*u, resource.FetchOptions{}) + + if err != nil { + // this is somewhat expected, server is likely not reachable + continue + } + + fetchedAdv := string(data) + // if the server is reachable, lets check to make sure the keys match + if fetchedAdv != *tang.Advertisement { + return fmt.Errorf("tang advertisement does not match: %v", err) + } + } + + // lets bind our device if _, err := s.Logger.LogCmd( exec.Command(distro.ClevisCmd(), "luks", "bind", "-f", "-k", keyFilePath, "-d", devAlias, pin, config), "Clevis bind", ); err != nil { return fmt.Errorf("binding clevis device: %v", err) } - // close & re-open Clevis devices to make sure that we can unlock them - if _, err := s.Logger.LogCmd( - exec.Command(distro.CryptsetupCmd(), "luksClose", luks.Name), - "closing clevis luks device %v", luks.Name, - ); err != nil { - return fmt.Errorf("closing luks device: %v", err) - } - if _, err := s.Logger.LogCmd( - exec.Command(distro.ClevisCmd(), "luks", "unlock", "-d", devAlias, "-n", luks.Name), - "reopening clevis luks device %s", luks.Name, - ); err != nil { - return fmt.Errorf("reopening luks device %s: %v", luks.Name, err) + // since the number of tang servers that are "online" is greater then the threshold, we can safely close and re-open the device + // otherwise we need to do an offline first boot and forgo the close and re-open + if len(tangServersWithoutAdvertisement) > *luks.Clevis.Threshold { + + // close & re-open Clevis devices to make sure that we can unlock them + if _, err := s.Logger.LogCmd( + exec.Command(distro.CryptsetupCmd(), "luksClose", luks.Name), + "closing clevis luks device %v", luks.Name, + ); err != nil { + return fmt.Errorf("closing luks device: %v", err) + } + if _, err := s.Logger.LogCmd( + exec.Command(distro.ClevisCmd(), "luks", "unlock", "-d", devAlias, "-n", luks.Name), + "reopening clevis luks device %s", luks.Name, + ); err != nil { + return fmt.Errorf("reopening luks device %s: %v", luks.Name, err) + } + } } diff --git a/internal/exec/stages/fetch_offline/fetch-offline.go b/internal/exec/stages/fetch_offline/fetch-offline.go index 7563992f99..c028191a98 100644 --- a/internal/exec/stages/fetch_offline/fetch-offline.go +++ b/internal/exec/stages/fetch_offline/fetch-offline.go @@ -91,6 +91,10 @@ func configNeedsNetRecurse(v reflect.Value) (bool, error) { case t == reflect.TypeOf(types.Resource{}): return sourceNeedsNet(v.Interface().(types.Resource)) case t == reflect.TypeOf(types.Tang{}): + tang := v.Interface().(types.Tang) + if !cfgutil.NilOrEmpty(tang.Advertisement) { + return false, nil + } return true, nil case t == reflect.TypeOf(types.ClevisCustom{}): cc := v.Interface().(types.ClevisCustom) diff --git a/internal/exec/stages/fetch_offline/fetch_offline_test.go b/internal/exec/stages/fetch_offline/fetch_offline_test.go index d0e14bb576..b593a1be2e 100644 --- a/internal/exec/stages/fetch_offline/fetch_offline_test.go +++ b/internal/exec/stages/fetch_offline/fetch_offline_test.go @@ -71,6 +71,47 @@ func TestConfigNotNeedsNet(t *testing.T) { }, // Empty Config {}, + + // Tang with adv set does not need networking on first boot. + { + Storage: types.Storage{ + Luks: []types.Luks{ + { + Name: "foobar", + Device: util.StrToPtr("foo"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + Thumbprint: util.StrToPtr("mythumbprint"), + URL: "http://mytang.example.com", + Advertisement: util.StrToPtr(" {\"payload\": \"...\",\"protected\":\"...\",\"signature\":\"...\"}"), + }, + }, + }, + }, + }, + }, + }, + // Tang with advertisement set does not need networking on first boot. + { + Storage: types.Storage{ + Luks: []types.Luks{ + { + Name: "foobar", + Device: util.StrToPtr("foo"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + Thumbprint: util.StrToPtr("mythumbprint"), + URL: "http://mytang.example.com", + Advertisement: util.StrToPtr(" {\"payload\": \"...\",\"protected\":\"...\",\"signature\":\"...\"}"), + }, + }, + }, + }, + }, + }, + }, } for i, test := range tests { @@ -80,7 +121,7 @@ func TestConfigNotNeedsNet(t *testing.T) { func TestConfigNeedsNet(t *testing.T) { tests := []types.Config{ - // Tang + // Tang with no adv set needs networking on first boot. { Storage: types.Storage{ Luks: []types.Luks{ @@ -126,6 +167,76 @@ func TestConfigNeedsNet(t *testing.T) { }, }, }, + // Tang with adv explicitly set to nil needs networking on first boot. + { + Storage: types.Storage{ + Luks: []types.Luks{ + { + Name: "foobar", + Device: util.StrToPtr("foo"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + Thumbprint: util.StrToPtr("mythumbprint"), + URL: "http://mytang.example.com", + Advertisement: nil, + }, + }, + }, + }, + }, + }, + }, + // Multiple Tangs; one with adv set, one without needs networking on first boot. + { + Storage: types.Storage{ + Luks: []types.Luks{ + { + Name: "foobar", + Device: util.StrToPtr("foo"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + Thumbprint: util.StrToPtr("mythumbprint"), + URL: "http://mytang.example.com", + Advertisement: util.StrToPtr(" {\"payload\": \"...\",\"protected\":\"...\",\"signature\":\"...\"}"), + }, + { + Thumbprint: util.StrToPtr("mythumbprint"), + URL: "http://mytang.example.com", + Advertisement: nil, + }, + }, + }, + }, + }, + }, + }, + // Multiple Tangs with no adv set needs networking on first boot. + { + Storage: types.Storage{ + Luks: []types.Luks{ + { + Name: "foobar", + Device: util.StrToPtr("foo"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + Thumbprint: util.StrToPtr("mythumbprint"), + URL: "http://mytang.example.com", + Advertisement: util.StrToPtr(""), + }, + { + Thumbprint: util.StrToPtr("mythumbprint"), + URL: "http://mytang.example.com", + Advertisement: nil, + }, + }, + }, + }, + }, + }, + }, } for i, test := range tests {