Skip to content
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

[RFC] schema: add storage.encryption section #515

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions config/types/encryption.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright 2018 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package types

import (
"errors"
"fmt"

"github.com/coreos/ignition/config/validate/report"
)

const (
// encDevicesMax is the maximum number of encrypted volumes that can be
// created at once. It is an arbitrary sane number for input validation,
// it can be bumped if required.
encDevicesMax = 1024
// encKeyslotsMax is the maximux number of keyslots allowed per encrypted volume
encKeyslotsMax = 1 // TODO(lucab): this could be expanded to 8
)

var (
// ErrNoKeyslots is reported when 0 keyslots are specified
ErrNoKeyslots = errors.New("no keyslots specified")
// ErrNoKeyslotConfig is reported when a keyslot has no configured source
ErrNoKeyslotConfig = errors.New("keyslot is missing provider configuration")
// ErrTooManyKeyslotConfigs is reported when a keyslot has too many configured sources
ErrTooManyKeyslotConfigs = errors.New("keyslot has multiple provider configurations")
// ErrNoDevmapperName is reported when no device-mapper name is specified
ErrNoDevmapperName = errors.New("missing device-mapper name")
// ErrNoDevicePath is reported when no device path is specified
ErrNoDevicePath = errors.New("missing device path")
// ErrTooManyDevices is reported when too many devices are specified
ErrTooManyDevices = fmt.Errorf("too many devices specified, at most %d allowed", encDevicesMax)
// ErrTooManyKeyslots is reported when too many keyslots are specified
ErrTooManyKeyslots = fmt.Errorf("too many keyslots specified, at most %d allowed", encKeyslotsMax)
)

// ValidateKeySlots validates the "keySlots" field
func (e Encryption) ValidateKeySlots() report.Report {
r := report.Report{}
if len(e.KeySlots) == 0 {
r.Add(report.Entry{
Message: ErrNoKeyslots.Error(),
Kind: report.EntryError,
})
}
if len(e.KeySlots) > encKeyslotsMax {
r.Add(report.Entry{
Message: ErrTooManyKeyslots.Error(),
Kind: report.EntryError,
})
}

for _, ks := range e.KeySlots {
ksConfigured := 0
if ks.Content != nil {
ksConfigured++
}
// TODO(lucab): validate new providers here.
if ksConfigured == 0 {
r.Add(report.Entry{
Message: ErrNoKeyslotConfig.Error(),
Kind: report.EntryError,
})
} else if ksConfigured > 1 {
r.Add(report.Entry{
Message: ErrTooManyKeyslotConfigs.Error(),
Kind: report.EntryError,
})
}
}

return r
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should add a func (c Content) ValidateSource() report.Report that calls validateUrl (or maybe something else since this only supports https?) on c.Source.

Copy link
Contributor Author

@lucab lucab Feb 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've slightly bent validateURL to take an additional optional subset of whitelisted schemes, let me know if you prefer something simpler and dedicated instead.


// ValidateName validates the "name" field
func (e Encryption) ValidateName() report.Report {
r := report.Report{}
if e.Name == "" {
r.Add(report.Entry{
Message: ErrNoDevmapperName.Error(),
Kind: report.EntryError,
})
}
return r
}

// ValidateDevice validates the "device" field
func (e Encryption) ValidateDevice() report.Report {
r := report.Report{}
if e.Device == "" {
r.Add(report.Entry{
Message: ErrNoDevicePath.Error(),
Kind: report.EntryError,
})
}
return r
}

// ValidateSource validates the "source" field
func (c Content) ValidateSource() report.Report {
r := report.Report{}
if err := validateURL(c.Source, "https"); err != nil {
r.Add(report.Entry{
Message: err.Error(),
Kind: report.EntryError,
})
}
return r
}
113 changes: 113 additions & 0 deletions config/types/encryption_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright 2018 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package types

import (
"reflect"
"testing"

"github.com/coreos/ignition/config/validate/report"
)

func TestEncryptionValidateName(t *testing.T) {
simpleSlots := []Keyslot{
{
Content: &Content{Source: "https://localhost/key.txt"},
},
}

tests := []struct {
in Encryption
out error
}{
{
in: Encryption{Name: "foo", Device: "/dev/bar", KeySlots: simpleSlots},
out: nil,
},
{
in: Encryption{Name: "", Device: "/dev/bar", KeySlots: simpleSlots},
out: ErrNoDevmapperName,
},
}

for i, tt := range tests {
err := tt.in.ValidateName()
if !reflect.DeepEqual(report.ReportFromError(tt.out, report.EntryError), err) {
t.Errorf("#%d: bad error: want %v, got %v", i, tt.out, err)
}
}
}

func TestEncryptionValidateDevice(t *testing.T) {
simpleSlots := []Keyslot{
{
Content: &Content{Source: "https://localhost/key.txt"},
},
}

tests := []struct {
in Encryption
out error
}{
{
in: Encryption{Name: "foo", Device: "/dev/bar", KeySlots: simpleSlots},
out: nil,
},
{
in: Encryption{Name: "foo", Device: "", KeySlots: simpleSlots},
out: ErrNoDevicePath,
},
}

for i, tt := range tests {
err := tt.in.ValidateDevice()
if !reflect.DeepEqual(report.ReportFromError(tt.out, report.EntryError), err) {
t.Errorf("#%d: bad error: want %v, got %v", i, tt.out, err)
}
}
}

func TestEncryptionValidateKeySlots(t *testing.T) {
simpleSlots := []Keyslot{
{
Content: &Content{Source: "https://localhost/key.txt"},
},
}

tests := []struct {
in Encryption
out error
}{
{
in: Encryption{Name: "foo", Device: "/dev/bar", KeySlots: simpleSlots},
out: nil,
},
{
in: Encryption{Name: "foo", Device: "/dev/bar", KeySlots: []Keyslot{}},
out: ErrNoKeyslots,
},
{
in: Encryption{Name: "foo", Device: "/dev/bar", KeySlots: []Keyslot{{}}},
out: ErrNoKeyslotConfig,
},
}

for i, tt := range tests {
err := tt.in.ValidateKeySlots()
if !reflect.DeepEqual(report.ReportFromError(tt.out, report.EntryError), err) {
t.Errorf("#%d: bad error: want %v, got %v", i, tt.out, err)
}
}
}
17 changes: 17 additions & 0 deletions config/types/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ type ConfigReference struct {
Verification Verification `json:"verification,omitempty"`
}

type Content struct {
Source string `json:"source,omitempty"`
}

type Create struct {
Force bool `json:"force,omitempty"`
Options []CreateOption `json:"options,omitempty"`
Expand All @@ -44,6 +48,14 @@ type Disk struct {
WipeTable bool `json:"wipeTable,omitempty"`
}

type Encryption struct {
Device string `json:"device"`
DisableDiscard bool `json:"disableDiscard,omitempty"`
KeySlots []Keyslot `json:"keySlots"`
Name string `json:"name"`
WipeVolume bool `json:"wipeVolume,omitempty"`
}

type File struct {
Node
FileEmbedded1
Expand Down Expand Up @@ -81,6 +93,10 @@ type IgnitionConfig struct {
Replace *ConfigReference `json:"replace,omitempty"`
}

type Keyslot struct {
Content *Content `json:"content,omitempty"`
}

type Link struct {
Node
LinkEmbedded1
Expand Down Expand Up @@ -193,6 +209,7 @@ type Security struct {
type Storage struct {
Directories []Directory `json:"directories,omitempty"`
Disks []Disk `json:"disks,omitempty"`
Encryption []Encryption `json:"encryption,omitempty"`
Files []File `json:"files,omitempty"`
Filesystems []Filesystem `json:"filesystems,omitempty"`
Links []Link `json:"links,omitempty"`
Expand Down
18 changes: 15 additions & 3 deletions config/types/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ import (
)

var (
// ErrInvalidScheme is returned when the scheme is not valid for the context.
ErrInvalidScheme = errors.New("invalid url scheme")
)

func validateURL(s string) error {
func validateURL(s string, subset ...string) error {
// Empty url is valid, indicates an empty file
if s == "" {
return nil
Expand All @@ -37,13 +38,24 @@ func validateURL(s string) error {

switch u.Scheme {
case "http", "https", "oem", "tftp", "s3":
return nil
break
case "data":
if _, err := dataurl.DecodeString(s); err != nil {
return err
}
return nil
break
default:
return ErrInvalidScheme
}

if len(subset) != 0 {
for _, allowed := range subset {
if u.Scheme == allowed {
return nil
}
}
return ErrInvalidScheme
}

return nil
}
8 changes: 8 additions & 0 deletions doc/configuration-v2_3-experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ The Ignition configuration is a JSON document conforming to the following specif
* **_start_** (integer): the start of the partition (in device logical sectors). If zero, the partition will be positioned at the start of the largest block available.
* **_typeGuid_** (string): the GPT [partition type GUID][part-types]. If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data).
* **_guid_** (string): the GPT unique partition GUID.
* **_encryption_** (list of objects): the list of encrypted volumes to be configured and their options.
* **name** (string): name for the device-mapper volume to create.
* **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks.
* **_disableDiscard_** (boolean): whether to disable TRIM/discard passthrough. Defaults to `false`.
* **_wipeVolume_** (boolean): whether to wipe the device before creating the volume. Defaults to `false`. See [operator notes](operator-notes.md#reuse-semantics-for-encrypted-volumes) for more details.
* **keySlots** (list of objects): the list of keyslots to be configured and their options. Currently, it must contain exactly one entry.
* **_content_** (object):
* **source** (string): the URL of the passphrase. Supported schemes are: `https`.
* **_raid_** (list of objects): the list of RAID arrays to be configured.
* **name** (string): the name to use for the resulting md device.
* **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.).
Expand Down
9 changes: 9 additions & 0 deletions doc/operator-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,12 @@ In the second two cases, where there is a preexisting filesystem, Ignition's beh
If `wipeFilesystem` is set to true, Ignition will always wipe any preexisting filesystem and create the desired filesystem. Note this will result in any data on the old filesystem being lost.

If `wipeFilesystem` is set to false, Ignition will then attempt to reuse the existing filesystem. If the filesystem is of the correct type, has a matching label, and has a matching UUID, then Ignition will reuse the filesystem. If the label or UUID is not set in the Ignition config, they don't need to match for Ignition to reuse the filesystem. Any preexisting data will be left on the device and will be available to the installation. If the preexisting filesystem is *not* of the correct type, then Ignition will fail, and the machine will fail to boot.

## Reuse Semantics for Encrypted Volumes

Similarly to filesystems, Ignition can be instructed to reuse pre-existing encrypted volumes via the `wipeVolume` flag.

If `wipeVolume` is set to `true`, the specified device will be forcibly wiped and a new volume initialized on it. This implies that any data previously stored on the device, either encrypted or plaintext, will be lost.

If `wipeVolume` is set to `false` (default), Ignition will try to reuse the volume if the specified device has a matching GPT partition type GUID. The preexisting volume will be unlocked before proceeding.
If the device does not have a matching partition type or if volume cannot be unlocked, then Ignition will fail, and the provisioning process will be aborted.
Loading