Compare commits

...

98 Commits

Author SHA1 Message Date
Dimitris Karakasilis
da93e626c5 Merge pull request #43 from kairos-io/1836-more-logs
1836 more logs
2023-10-27 09:20:27 +03:00
Dimitris Karakasilis
ecbbe1499e Add more logs and refactor the server handers
- Flatten if/else logic by handling errors and returning early
- Use different logger for server logs. Also handle skipped errors.
- Remove unecessary for loop
- --zap-log-level can already be used (and it works)
- Remove non-existent enki flag
- Run tests with KVM enabled on self-hosted runners
  and also don't add grub.cfg since it's already there in the base image
- Remove non-used earthly target

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-10-27 09:17:48 +03:00
Mauro Morales
09981d750e Configure automerge for patch updates 2023-10-04 16:56:52 +02:00
Itxaka
b5b4d0d042 🤖 Add concurrency to CI (#33) 2023-06-15 11:20:19 +02:00
renovate[bot]
8420155746 Update docker/build-push-action action to v4 (#32)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-15 11:15:24 +02:00
renovate[bot]
a9359bf713 Update actions/checkout action to v3 (#31)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-15 11:15:01 +02:00
renovate[bot]
b31467e925 Update module github.com/mudler/yip to v1.2.0 (#28)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-15 11:14:48 +02:00
renovate[bot]
c5dc8db56b Update module github.com/jaypipes/ghw to v0.11.0 (#26)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-15 11:10:42 +02:00
renovate[bot]
a80703a556 Update module github.com/onsi/gomega to v1.27.8 (#24)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-15 11:10:22 +02:00
renovate[bot]
0b6f771d32 Update module github.com/kairos-io/kcrypt to v0.7.0 (#27)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-15 11:08:46 +02:00
renovate[bot]
72dd7d3e50 Update module github.com/onsi/ginkgo/v2 to v2.10.0 (#29)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-15 11:08:11 +02:00
Itxaka
0619047a20 Drop kairos and use sdk for collector (#20) 2023-06-15 09:35:01 +02:00
renovate[bot]
715664969a Add renovate.json (#6)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-06-14 14:38:01 +02:00
Mauro Morales
bcda5b5b38 Update issue templates
relates to https://github.com/kairos-io/kairos/issues/1483
2023-06-13 12:17:08 +02:00
Itxaka
b2a0330dd8 Fix lint
Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-05-10 00:31:15 +02:00
Itxaka
0b68d90081 Bump ghw and fix label (#17)
* Bump ghw and fix label

old label was the new FilesystemLabel. Now the label refers to the
partition label which is different

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>

* bump deps

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>

* Rework ginkgo

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>

* docker login

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>

* [Will drop]Allow building kcrypt from branches

Otherwise any changes that need both wont pass tests.

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>

* Dont build the iso 5 times

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>

* This confirms Im dumb and dont know how to program

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>

* debug logs

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>

* debug

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>

* 🤖 run in github CI

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>

* Debug

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>

* debug

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>

* Add /tmp/oem to scan dirs for config

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>

---------

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
2023-05-10 00:24:58 +02:00
Itxaka
40267d4c24 Merge pull request #13 from kairos-io/bump-go-version-to-1.20.2
⬆️ Bump go to 1.20
2023-03-30 21:37:09 +02:00
Mauro Morales
e0ae7a12a4 Remove golint cuz it has some issues with cgo
Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
2023-03-30 09:21:09 +02:00
Mauro Morales
c2c50877da Remove patch number from defined go version
Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
2023-03-30 09:19:58 +02:00
Mauro Morales
7a2627fcc8 Add go version to workflow
Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
2023-03-29 15:47:00 +02:00
Mauro Morales
a0c4462f99 Update setup-go to v4
Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
2023-03-29 15:44:09 +02:00
Mauro Morales
145dd400b1 Add lint workflow
Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
2023-03-29 15:33:27 +02:00
Mauro Morales
95dbb0d0be Add yamllint
Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
2023-03-29 14:33:29 +02:00
Mauro Morales
0e56e52cbf Bump go to 1.20
Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
2023-03-29 14:28:45 +02:00
Dimitris Karakasilis
3a22134226 Run tests with sudo to be able to access /dev/kvm
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-20 12:33:44 +02:00
Dimitris Karakasilis
7b561efed8 Try KVM on CI
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-17 17:59:38 +02:00
Dimitris Karakasilis
6ff6262459 Configure earthly to use the docker mirror in CI
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-17 17:11:26 +02:00
Dimitris Karakasilis
816013d33d Don't use the earthly script
in order to avoid nested docker and use the deployed docker mirror

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-17 16:06:51 +02:00
Dimitris Karakasilis
8d0fb0148d export KUBECONFIG so that it's set when running commands in go tests
Error:

```
  [FAILED] Error from server (NotFound): namespaces "actions-runner-system" not found
```
coming from: /runner/_work/kcrypt-challenger/kcrypt-challenger/tests/encryption_test.go:157

is suspicious. That namespace shouldn't exist in the test k3d cluster,
no idea why it was looked up. I suspect the env for the following
command somehow pointed to the "outer" cluster:

```
cmd := exec.Command("kubectl", "get", "secrets", ...)
```

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-17 15:53:02 +02:00
Dimitris Karakasilis
ffd5f18bcf Fix error interface conversion: interface {} is []uint8, not string
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-17 15:20:18 +02:00
Dimitris Karakasilis
3b89def5b4 Make sure we run command in bash to avoid error in Ubuntu
```
set: Illegal option -o pipefail
```

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-17 12:38:22 +02:00
Dimitris Karakasilis
887d67907b Avoid the host cluster CIDR to let DNS work in k3d
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-17 11:52:14 +02:00
Dimitris Karakasilis
b0a7aa5fdf Revert "Try to fix the MTU problem in a hackish way (tmp)"
This reverts commit 40875bbae1.
2023-02-16 11:14:30 +02:00
Dimitris Karakasilis
40875bbae1 Try to fix the MTU problem in a hackish way (tmp)
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-16 10:36:45 +02:00
Dimitris Karakasilis
7166b14c7e Revert "Bump earthly"
This reverts commit 9eb5d9b086.
2023-02-16 09:57:25 +02:00
Dimitris Karakasilis
9eb5d9b086 Bump earthly
hoping to get this fix:
https://github.com/earthly/earthly/issues/1934#issuecomment-1160819298

and see if it makes any difference

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-16 09:53:30 +02:00
Dimitris Karakasilis
4da6a4f3b0 "Modernize" the +iso target
according to this example:
4e2dd37e70/Earthfile (L114)

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-16 09:08:15 +02:00
Dimitris Karakasilis
74fc9c62b4 Switch to ubuntu because opensuse repos time out
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-15 18:41:15 +02:00
Dimitris Karakasilis
f3f10b4919 Don't prompt
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-15 18:10:01 +02:00
Dimitris Karakasilis
3d4829859b Run e2e tests on self-hosted runners
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-15 17:52:14 +02:00
Dimitris Karakasilis
8a17ff714c Merge pull request #12 from kairos-io/346-tls-support
346 tls support
2023-02-15 12:28:20 +02:00
Dimitris Karakasilis
27114b8db8 Run e2e tests without earthly
getting closer to running them with KVM enabled. This will require self
hosted runners with KVM enabled but we will get there eventually.

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-15 11:00:24 +02:00
Dimitris Karakasilis
1e3efb57cc Split scenarios in different GA jobs
to parallelise better and allow re-running just the failed tests

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-15 09:58:16 +02:00
Dimitris Karakasilis
0c236b6145 Let OnFailure handle abnormal VM termination
now that peg gracefully terminates the VM when `Destroy` is called.

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-14 16:15:11 +02:00
Dimitris Karakasilis
d390f77688 Bump peg (after merging PR#9)
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-14 10:48:08 +02:00
Dimitris Karakasilis
266c4f20e9 Handle unexpected VM exit better and use a core image with working DNS
Also print serial output when something goes wrong

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-14 10:09:46 +02:00
Dimitris Karakasilis
4c0b40d3a0 Add gettext-runtime in Earthly image
which provides the `envsubst` command needed in the e2e test script

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-09 12:10:38 +02:00
Dimitris Karakasilis
08bb62f94e Remove TODO
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-09 12:03:24 +02:00
Dimitris Karakasilis
0d3406fa7b Fallback to system CAs
No automated test for this case because it's complicated to get a
properly signed certificate in tests:

- the domain we use is sslip.io (not sure if letsencrypt would sign it)
- we need to use the letsencrypt production and that has quotas not
  suitable for CI

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-09 11:48:59 +02:00
Dimitris Karakasilis
1cd4d9a7af Implement test that checks invalid cert case
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-09 11:48:59 +02:00
Dimitris Karakasilis
d875e54171 Implement pinned certs
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-09 11:48:59 +02:00
Dimitris Karakasilis
2967fb0a6c [WIP] Implement failing test for pinned cert
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-09 11:48:59 +02:00
Dimitris Karakasilis
e9433d2ba7 Move challenger server inside the cluster and serve with TLS
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-09 11:48:59 +02:00
Ettore Di Giacinto
7abdc7b092 📖 Update README 2023-02-07 12:29:13 +01:00
Dimitris Karakasilis
9448ecdd54 Ignore README changes in e2e workflow
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-02 12:06:19 +02:00
Dimitris Karakasilis
d8cd48b411 Fix link in README to send users directly to "main" runs
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-02 12:03:55 +02:00
Dimitris Karakasilis
43d629c974 Show "main" status in badge
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-02 12:02:02 +02:00
Dimitris Karakasilis
b00d3af43b Rename "master" to "main" so that e2e tests run correctly
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-02 11:58:12 +02:00
Dimitris Karakasilis
7d83e07b05 Fix typo in badge url
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-02 11:56:38 +02:00
Dimitris Karakasilis
2fe3f3bc00 Add badges to the README
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-02 11:54:45 +02:00
Dimitris Karakasilis
791d9dbb8b Merge pull request #11 from kairos-io/e2e-tests
E2e tests
2023-02-02 11:49:35 +02:00
Dimitris Karakasilis
7dc1e39ac7 Implement an e2e test suite for kcrypt encryption
Scenarios based on docs: https://kairos.io/docs/advanced/partition_encryption/

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-02-02 11:48:44 +02:00
mudler
076a50b2e9 Drop unnecessary condition
Signed-off-by: mudler <mudler@c3os.io>
2023-01-24 17:53:38 +01:00
mudler
f8e7a0df87 Revert "Change function return style"
This reverts commit 968ff53267.
2023-01-24 17:40:00 +01:00
mudler
968ff53267 Change function return style
Signed-off-by: mudler <mudler@c3os.io>
2023-01-24 16:19:33 +01:00
mudler
a95436bf16 Clean up default secret names 2023-01-24 12:16:09 +01:00
mudler
dfe29aa24f Return a payload
Signed-off-by: mudler <mudler@c3os.io>
2023-01-24 12:03:08 +01:00
mudler
db2b6758de 🌱 Handle case when secret doesn't exist
Signed-off-by: mudler <mudler@c3os.io>
2023-01-23 23:00:16 +01:00
Dimitris Karakasilis
317c6d87b4 Merge pull request #10 from kairos-io/local_encryption
🌱 Enable local encryption, remote now partially uses TPM
2023-01-19 16:27:52 +02:00
Dimitris Karakasilis
8898eb8ae9 Small refactorings (renaming vars, create constants etc)
Signed-off-by: Ettore Di Giacinto <ettore@spectrocloud.com>
2023-01-19 16:24:39 +02:00
Ettore Di Giacinto
91c24586ea Improve naming of functions and add comments
Signed-off-by: Dimitris Karakasilis <dimitris@spectrocloud.com>
2023-01-19 16:06:53 +02:00
Dimitris Karakasilis
eefd5f2c2c Extract method and simplify "if" logic
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-01-19 15:46:35 +02:00
mudler
83f529b53d 🌱 Small fixups
Signed-off-by: mudler <mudler@c3os.io>
2023-01-19 14:24:33 +01:00
mudler
2c8a589906 Enable local encryption, remote now partially uses TPM
Signed-off-by: mudler <mudler@c3os.io>
2023-01-18 23:32:27 +01:00
Dimitris Karakasilis
9f7abe321a Merge pull request #9 from kairos-io/use_tpm_helpers
Use tpm helpers
2023-01-18 17:26:15 +02:00
mudler
2603757f2c Simplify challenge
Signed-off-by: mudler <mudler@c3os.io>
2023-01-18 16:09:52 +01:00
mudler
df0fb4a341 ⬆️ Point to tpm-helpers
Signed-off-by: mudler <mudler@c3os.io>
2023-01-18 16:02:17 +01:00
Dimitris Karakasilis
12edf4d4cf Merge pull request #8 from kairos-io/399-configuration-from-file
399 configuration from file
2023-01-18 16:58:38 +02:00
Dimitris Karakasilis
b3ca9687c6 Implement test and remove TODOs
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-01-18 16:56:49 +02:00
Dimitris Karakasilis
72829108df Extract client code to separate package and test it
- add new suite to the pipeline and fix Earthly to run tests
- read configuration from file
- the "kcrypt" section is our configuration now
- move configuration logic in `kcrypt` repository

Part of #399

Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-01-18 15:25:04 +02:00
Dimitris Karakasilis
a49495e47a Merge pull request #7 from kairos-io/380-traceback-partition
380 traceback partition
2022-11-17 15:06:04 +02:00
Dimitris Karakasilis
83bba2f0cf Introduce a test suite and an earthly target to run it
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2022-11-17 12:57:09 +02:00
Dimitris Karakasilis
3b9477b6ea Add omitempty on PartitionSpec fields to make the optional
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2022-11-17 12:56:59 +02:00
Dimitris Karakasilis
aa736211af Don't go frenzy when a TPM is not found. Just return.
Because there is no guarantee that a TPM will eventually be found.

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2022-11-11 09:54:19 +02:00
Dimitris Karakasilis
7a07d5c45b Change sealedvolume CRD to add more fields to the partition
We use those field to identify which partition is requested. On the
client side, the label is not available when the partition is encrypted.
We allow the client to request the passphrase for a partition using the
partition name (e.g. /dev/sdb1) or the UUID (as returned by blkid).

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2022-11-11 09:54:19 +02:00
Dimitris Karakasilis
a3df62df03 [WIP] Send more data over to the escrow server
in order to identify the partition. The label is not available before
the filesystem is descrypted (post-install). In that case the server can
look up the partition in the configuration using the name or the
mountpoint.

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2022-11-11 09:54:19 +02:00
mudler
9e8249c730 Minor fixups 2022-10-18 17:04:48 +02:00
mudler
4236420ed5 📖 Update README 2022-10-18 15:44:09 +02:00
mudler
21681a58fd Dial-in re-attempts 2022-10-18 15:43:58 +02:00
Ettore Di Giacinto
a2cb5d95fb Skip errors when evaluating cmdline (best-effort) 2022-10-18 12:27:48 +00:00
Ettore Di Giacinto
06b8dc9c58 🐛 Fixup unmarshal to anonymous struct 2022-10-17 22:27:39 +02:00
Ettore Di Giacinto
d9da1b4090 Merge pull request #1 from kairos-io/tests
Add Earthfile
2022-10-17 22:10:26 +02:00
Ettore Di Giacinto
770814996b Attempt to get also from part name 2022-10-17 19:08:45 +00:00
Ettore Di Giacinto
a00353fda1 Add Earthfile 2022-10-17 16:44:25 +02:00
Ettore Di Giacinto
673bfcbd56 Slightly change spec 2022-10-13 22:21:06 +00:00
Ettore Di Giacinto
7c6fa7df06 🎨 Small fixups 2022-10-13 21:35:26 +00:00
Ettore Di Giacinto
6124f9aec9 🤖 Fixup workflow 2022-10-13 20:35:58 +00:00
Ettore Di Giacinto
05d48347d7 Initial import 2022-10-13 20:34:44 +00:00
77 changed files with 5274 additions and 0 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
# Ignore build and test binaries.
bin/
testbin/

View File

@@ -0,0 +1,12 @@
---
name: File issues on main Kairos repo
about: Tell users to file their issues on the main Kairos repo
title: ''
labels: ''
assignees: ''
---
:warning: All Kairos issues are tracked in our main repo, please file your issue there, thanks! :warning:
https://github.com/kairos-io/kairos/issues

115
.github/workflows/e2e-tests.yml vendored Normal file
View File

@@ -0,0 +1,115 @@
name: End to end tests
on:
push:
paths-ignore:
- 'README.md'
branches:
- main
pull_request:
paths-ignore:
- 'README.md'
concurrency:
group: ci-e2e-${{ github.head_ref || github.ref }}-${{ github.repository }}
cancel-in-progress: true
jobs:
build-iso:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: ^1.20
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_LOGIN }}
password: ${{ secrets.DOCKER_PASS }}
- name: Install earthly
uses: Luet-lab/luet-install-action@v1
with:
repository: quay.io/kairos/packages
packages: utils/earthly
- name: build iso
run: |
# Configure earthly to use the docker mirror in CI
# https://docs.earthly.dev/ci-integration/pull-through-cache#configuring-earthly-to-use-the-cache
mkdir -p ~/.earthly/
cat << EOF > ~/.earthly/config.yml
global:
buildkit_additional_config: |
[registry."docker.io"]
mirrors = ["registry.docker-mirror.svc.cluster.local:5000"]
[registry."registry.docker-mirror.svc.cluster.local:5000"]
insecure = true
EOF
earthly -P +iso
- uses: actions/upload-artifact@v3
with:
name: challenger.iso.zip
path: |
build/*.iso
e2e-tests:
needs:
- build-iso
runs-on: self-hosted
strategy:
fail-fast: false
matrix:
include:
- label: "local-encryption"
- label: "remote-auto"
- label: "remote-static"
- label: "remote-https-pinned"
- label: "remote-https-bad-cert"
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: ^1.20
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_LOGIN }}
password: ${{ secrets.DOCKER_PASS }}
- name: Install deps
run: |
curl -L https://github.com/mudler/luet/releases/download/0.33.0/luet-0.33.0-linux-amd64 -o luet
chmod +x luet
sudo mv luet /usr/bin/luet
sudo mkdir -p /etc/luet/repos.conf.d/
sudo luet repo add -y kairos --url quay.io/kairos/packages --type docker
LUET_NOLOCK=true sudo -E luet install -y container/kubectl utils/k3d utils/earthly
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: challenger.iso.zip
- name: Run tests
env:
LABEL: ${{ matrix.label }}
KVM: true
run: |
sudo apt update && \
sudo apt install -y git qemu-system-x86 qemu-utils swtpm jq make glibc-tools \
openssl curl gettext ca-certificates curl gnupg lsb-release
export ISO=$PWD/$(ls *.iso)
# We run with sudo to be able to access /dev/kvm
./scripts/e2e-tests.sh
- uses: actions/upload-artifact@v3
if: failure()
with:
name: ${{ matrix.label }}-test.logs.zip
path: tests/**/logs/*
if-no-files-found: warn

68
.github/workflows/image.yml vendored Normal file
View File

@@ -0,0 +1,68 @@
---
name: 'build container images'
on:
push:
branches:
- main
tags:
- '*'
concurrency:
group: ci-image-${{ github.head_ref || github.ref }}-${{ github.repository }}
cancel-in-progress: true
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Prepare
id: prep
run: |
DOCKER_IMAGE=quay.io/kairos/kcrypt-challenger
VERSION=latest
SHORTREF=${GITHUB_SHA::8}
# If this is git tag, use the tag name as a docker tag
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
fi
TAGS="${DOCKER_IMAGE}:${VERSION},${DOCKER_IMAGE}:${SHORTREF}"
# If the VERSION looks like a version number, assume that
# this is the most recent version of the image and also
# tag it 'latest'.
if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
TAGS="$TAGS,${DOCKER_IMAGE}:latest"
fi
# Set output parameters.
echo ::set-output name=tags::${TAGS}
echo ::set-output name=docker_image::${DOCKER_IMAGE}
- name: Set up QEMU
uses: docker/setup-qemu-action@master
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@master
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }}
- name: Build
uses: docker/build-push-action@v4
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.prep.outputs.tags }}

36
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Lint
on:
push:
branches:
- main
pull_request:
paths:
- '**'
concurrency:
group: ci-lint-${{ github.head_ref || github.ref }}-${{ github.repository }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: ^1.20
- name: Install earthly
uses: Luet-lab/luet-install-action@v1
with:
repository: quay.io/kairos/packages
packages: utils/earthly
- name: Run Lint checks
run: |
earthly +lint

24
.github/workflows/unit-tests.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
---
name: Unit tests
on:
push:
branches:
- master
pull_request:
concurrency:
group: ci-unit-${{ github.head_ref || github.ref }}-${{ github.repository }}
cancel-in-progress: true
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Run tests
run: |
./earthly.sh +test

26
.gitignore vendored Normal file
View File

@@ -0,0 +1,26 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
bin
testbin/*
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Kubernetes Generated files - skip generated files, except for vendored files
!vendor/**/zz_generated.*
# editor and IDE paraphernalia
.idea
*.swp
*.swo
*~
/helm-chart

21
.yamllint Normal file
View File

@@ -0,0 +1,21 @@
extends: default
rules:
# 80 chars should be enough, but don't fail if a line is longer
line-length:
max: 150
level: warning
# accept both key:
# - item
#
# and key:
# - item
indentation:
indent-sequences: whatever
truthy:
check-keys: false
document-start:
present: false

28
Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
# Build the manager binary
FROM golang:1.20 as builder
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download
# Copy the go source
COPY main.go main.go
COPY api/ api/
COPY pkg/ pkg/
COPY controllers/ controllers/
# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go
# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532
ENTRYPOINT ["/manager"]

103
Earthfile Normal file
View File

@@ -0,0 +1,103 @@
VERSION 0.6
ARG BASE_IMAGE=quay.io/kairos/core-ubuntu:latest
ARG OSBUILDER_IMAGE=quay.io/kairos/osbuilder-tools
# renovate: datasource=docker depName=golang
ARG GO_VERSION=1.20
ARG LUET_VERSION=0.33.0
build-challenger:
FROM golang:alpine
COPY . /work
WORKDIR /work
RUN CGO_ENABLED=0 go build -o kcrypt-discovery-challenger ./cmd/discovery
SAVE ARTIFACT /work/kcrypt-discovery-challenger kcrypt-discovery-challenger AS LOCAL kcrypt-discovery-challenger
image:
FROM $BASE_IMAGE
ARG IMAGE
COPY +build-challenger/kcrypt-discovery-challenger /system/discovery/kcrypt-discovery-challenger
SAVE IMAGE $IMAGE
image-rootfs:
FROM +image
SAVE ARTIFACT --keep-own /. rootfs
iso:
ARG OSBUILDER_IMAGE
ARG ISO_NAME=challenger
FROM $OSBUILDER_IMAGE
WORKDIR /build
COPY --keep-own +image-rootfs/rootfs /build/rootfs
RUN /entrypoint.sh --name $ISO_NAME --debug build-iso --squash-no-compression --date=false --output /build/ dir:/build/rootfs
SAVE ARTIFACT /build/$ISO_NAME.iso kairos.iso AS LOCAL build/$ISO_NAME.iso
SAVE ARTIFACT /build/$ISO_NAME.iso.sha256 kairos.iso.sha256 AS LOCAL build/$ISO_NAME.iso.sha256
test:
ARG GO_VERSION
FROM golang:$GO_VERSION
ENV CGO_ENABLED=0
WORKDIR /work
# Cache layer for modules
COPY go.mod go.sum ./
RUN go mod download && go mod verify
COPY . /work
RUN go run github.com/onsi/ginkgo/v2/ginkgo run --covermode=atomic --coverprofile=coverage.out -p -r pkg/challenger cmd/discovery/client
SAVE ARTIFACT coverage.out AS LOCAL coverage.out
# Generic targets
# usage e.g. ./earthly.sh +datasource-iso --CLOUD_CONFIG=tests/assets/qrcode.yaml
datasource-iso:
ARG OSBUILDER_IMAGE
ARG CLOUD_CONFIG
FROM $OSBUILDER_IMAGE
RUN zypper in -y mkisofs
WORKDIR /build
RUN touch meta-data
COPY ${CLOUD_CONFIG} user-data
RUN cat user-data
RUN mkisofs -output ci.iso -volid cidata -joliet -rock user-data meta-data
SAVE ARTIFACT /build/ci.iso iso.iso AS LOCAL build/datasource.iso
luet:
FROM quay.io/luet/base:$LUET_VERSION
SAVE ARTIFACT /usr/bin/luet /luet
e2e-tests-image:
FROM opensuse/tumbleweed
RUN zypper in -y go git qemu-x86 qemu-arm qemu-tools swtpm docker jq docker-compose make glibc libopenssl-devel curl gettext-runtime
ENV GOPATH="/go"
COPY . /test
WORKDIR /test
IF [ -e /test/build/kairos.iso ]
ENV ISO=/test/build/kairos.iso
ELSE
COPY +iso/kairos.iso kairos.iso
ENV ISO=/test/kairos.iso
END
COPY +luet/luet /usr/bin/luet
RUN mkdir -p /etc/luet/repos.conf.d/
RUN luet repo add -y kairos --url quay.io/kairos/packages --type docker
RUN LUET_NOLOCK=true luet install -y container/kubectl utils/k3d
e2e-tests:
FROM +e2e-tests-image
ARG LABEL
WITH DOCKER --allow-privileged
RUN ./scripts/e2e-tests.sh
END
lint:
BUILD +yamllint
yamllint:
FROM cytopia/yamllint
COPY . .
RUN yamllint .github/workflows/

260
Makefile Normal file
View File

@@ -0,0 +1,260 @@
# VERSION defines the project version for the bundle.
# Update this value when you upgrade the version of your project.
# To re-generate a bundle for another specific version without changing the standard setup, you can:
# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2)
# - use environment variables to overwrite this value (e.g export VERSION=0.0.2)
VERSION ?= 0.0.1
# CHANNELS define the bundle channels used in the bundle.
# Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable")
# To re-generate a bundle for other specific channels without changing the standard setup, you can:
# - use the CHANNELS as arg of the bundle target (e.g make bundle CHANNELS=candidate,fast,stable)
# - use environment variables to overwrite this value (e.g export CHANNELS="candidate,fast,stable")
ifneq ($(origin CHANNELS), undefined)
BUNDLE_CHANNELS := --channels=$(CHANNELS)
endif
# DEFAULT_CHANNEL defines the default channel used in the bundle.
# Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable")
# To re-generate a bundle for any other default channel without changing the default setup, you can:
# - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable)
# - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable")
ifneq ($(origin DEFAULT_CHANNEL), undefined)
BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL)
endif
BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL)
# IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images.
# This variable is used to construct full image tags for bundle and catalog images.
#
# For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both
# kairos.io/kcrypt-controller-bundle:$VERSION and kairos.io/kcrypt-controller-catalog:$VERSION.
IMAGE_TAG_BASE ?= quay.io/kairos/kcrypt-controller
# BUNDLE_IMG defines the image:tag used for the bundle.
# You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=<some-registry>/<project-name-bundle>:<tag>)
BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION)
# BUNDLE_GEN_FLAGS are the flags passed to the operator-sdk generate bundle command
BUNDLE_GEN_FLAGS ?= -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS)
# USE_IMAGE_DIGESTS defines if images are resolved via tags or digests
# You can enable this value if you would like to use SHA Based Digests
# To enable set flag to true
USE_IMAGE_DIGESTS ?= false
ifeq ($(USE_IMAGE_DIGESTS), true)
BUNDLE_GEN_FLAGS += --use-image-digests
endif
# Image URL to use all building/pushing image targets
IMG ?= controller:latest
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
ENVTEST_K8S_VERSION = 1.24.2
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif
# Setting SHELL to bash allows bash commands to be executed by recipes.
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
SHELL = /usr/bin/env bash -o pipefail
.SHELLFLAGS = -ec
.PHONY: all
all: build
##@ General
# The help target prints out all targets with their descriptions organized
# beneath their categories. The categories are represented by '##@' and the
# target descriptions by '##'. The awk commands is responsible for reading the
# entire set of makefiles included in this invocation, looking for lines of the
# file as xyz: ## something, and then pretty-format the target and help. Then,
# if there's a line with ##@ something, that gets pretty-printed as a category.
# More info on the usage of ANSI control characters for terminal formatting:
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
# More info on the awk command:
# http://linuxcommand.org/lc3_adv_awk.php
.PHONY: help
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Development
.PHONY: manifests
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
.PHONY: generate
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
.PHONY: fmt
fmt: ## Run go fmt against code.
go fmt ./...
.PHONY: vet
vet: ## Run go vet against code.
go vet ./...
.PHONY: test
test: manifests generate fmt vet envtest ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./pkg/... -coverprofile cover.out
##@ Build
.PHONY: build
build: generate fmt vet ## Build manager binary.
go build -o bin/manager main.go
.PHONY: run
run: manifests generate fmt vet ## Run a controller from your host.
go run ./main.go
.PHONY: docker-build
docker-build: test ## Build docker image with the manager.
docker build -t ${IMG} .
.PHONY: docker-push
docker-push: ## Push docker image with the manager.
docker push ${IMG}
##@ Deployment
ifndef ignore-not-found
ignore-not-found = true
endif
.PHONY: install
install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
$(KUSTOMIZE) build config/crd | kubectl apply -f -
.PHONY: uninstall
uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
$(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f -
.PHONY: deploy
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default | kubectl apply -f -
.PHONY: undeploy
undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
$(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f -
##@ Build Dependencies
## Location to install dependencies to
LOCALBIN ?= $(shell pwd)/bin
$(LOCALBIN):
mkdir -p $(LOCALBIN)
## Tool Binaries
KUSTOMIZE ?= $(LOCALBIN)/kustomize
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest
## Tool Versions
KUSTOMIZE_VERSION ?= v3.8.7
CONTROLLER_TOOLS_VERSION ?= v0.9.2
KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"
.PHONY: kustomize
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
$(KUSTOMIZE): $(LOCALBIN)
test -s $(LOCALBIN)/kustomize || { curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); }
.PHONY: controller-gen
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
$(CONTROLLER_GEN): $(LOCALBIN)
test -s $(LOCALBIN)/controller-gen || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION)
.PHONY: envtest
envtest: $(ENVTEST) ## Download envtest-setup locally if necessary.
$(ENVTEST): $(LOCALBIN)
test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
.PHONY: bundle
bundle: manifests kustomize ## Generate bundle manifests and metadata, then validate generated files.
operator-sdk generate kustomize manifests -q
cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG)
$(KUSTOMIZE) build config/manifests | operator-sdk generate bundle $(BUNDLE_GEN_FLAGS)
operator-sdk bundle validate ./bundle
.PHONY: bundle-build
bundle-build: ## Build the bundle image.
docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) .
.PHONY: bundle-push
bundle-push: ## Push the bundle image.
$(MAKE) docker-push IMG=$(BUNDLE_IMG)
.PHONY: opm
OPM = ./bin/opm
opm: ## Download opm locally if necessary.
ifeq (,$(wildcard $(OPM)))
ifeq (,$(shell which opm 2>/dev/null))
@{ \
set -e ;\
mkdir -p $(dir $(OPM)) ;\
OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \
curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.23.0/$${OS}-$${ARCH}-opm ;\
chmod +x $(OPM) ;\
}
else
OPM = $(shell which opm)
endif
endif
# A comma-separated list of bundle images (e.g. make catalog-build BUNDLE_IMGS=example.com/operator-bundle:v0.1.0,example.com/operator-bundle:v0.2.0).
# These images MUST exist in a registry and be pull-able.
BUNDLE_IMGS ?= $(BUNDLE_IMG)
# The image tag given to the resulting catalog image (e.g. make catalog-build CATALOG_IMG=example.com/operator-catalog:v0.2.0).
CATALOG_IMG ?= $(IMAGE_TAG_BASE)-catalog:v$(VERSION)
# Set CATALOG_BASE_IMG to an existing catalog image tag to add $BUNDLE_IMGS to that image.
ifneq ($(origin CATALOG_BASE_IMG), undefined)
FROM_INDEX_OPT := --from-index $(CATALOG_BASE_IMG)
endif
# Build a catalog image by adding bundle images to an empty catalog using the operator package manager tool, 'opm'.
# This recipe invokes 'opm' in 'semver' bundle add mode. For more information on add modes, see:
# https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator
.PHONY: catalog-build
catalog-build: opm ## Build a catalog image.
$(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT)
# Push the catalog image.
.PHONY: catalog-push
catalog-push: ## Push a catalog image.
$(MAKE) docker-push IMG=$(CATALOG_IMG)
CLUSTER_NAME?="kairos-challenger-e2e"
kind-setup:
kind create cluster --name ${CLUSTER_NAME} || true
$(MAKE) kind-setup-image
kind-setup-image: docker-build
kind load docker-image --name $(CLUSTER_NAME) $(IMG)
kind-prepare-tests: kind-setup install undeploy-dev deploy-dev
.PHONY: deploy-dev
deploy-dev: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG)
$(KUSTOMIZE) build config/dev | kubectl apply -f -
.PHONY: undeploy-dev
undeploy-dev: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
$(KUSTOMIZE) build config/dev | kubectl delete --ignore-not-found=$(ignore-not-found) -f -
kubesplit: manifests kustomize
rm -rf helm-chart
mkdir helm-chart
$(KUSTOMIZE) build config/default | kubesplit -helm helm-chart

19
PROJECT Normal file
View File

@@ -0,0 +1,19 @@
domain: kairos.io
layout:
- go.kubebuilder.io/v3
plugins:
manifests.sdk.operatorframework.io/v2: {}
scorecard.sdk.operatorframework.io/v2: {}
projectName: kcrypt-controller
repo: github.com/kairos-io/kairos-challenger
resources:
- api:
crdVersion: v1
namespaced: true
controller: true
domain: kairos.io
group: keyserver
kind: SealedVolume
path: github.com/kairos-io/kairos-challenger/api/v1alpha1
version: v1alpha1
version: "3"

92
README.md Normal file
View File

@@ -0,0 +1,92 @@
<h1 align="center">
<br>
<img width="184" alt="kairos-white-column 5bc2fe34" src="https://user-images.githubusercontent.com/2420543/193010398-72d4ba6e-7efe-4c2e-b7ba-d3a826a55b7d.png"><br>
Kcrypt challenger
<br>
</h1>
<h3 align="center">Kcrypt TPM challenger</h3>
<p align="center">
<a href="https://opensource.org/licenses/">
<img src="https://img.shields.io/badge/licence-APL2-brightgreen"
alt="license">
</a>
<a href="https://github.com/kairos-io/kcrypt-challenger/issues"><img src="https://img.shields.io/github/issues/kairos-io/kcrypt-challenger"></a>
<a href="https://kairos.io/docs/" target=_blank> <img src="https://img.shields.io/badge/Documentation-blue"
alt="docs"></a>
<img src="https://img.shields.io/badge/made%20with-Go-blue">
<img src="https://goreportcard.com/badge/github.com/kairos-io/kcrypt-challenger" alt="go report card" />
<a href="https://github.com/kairos-io/kcrypt-challenger/actions/workflows/e2e-tests.yml?query=branch%3Amain"> <img src="https://github.com/kairos-io/kcrypt-challenger/actions/workflows/e2e-tests.yml/badge.svg?branch=main"></a>
</p>
With Kairos you can build immutable, bootable Kubernetes and OS images for your edge devices as easily as writing a Dockerfile. Optional P2P mesh with distributed ledger automates node bootstrapping and coordination. Updating nodes is as easy as CI/CD: push a new image to your container registry and let secure, risk-free A/B atomic upgrades do the rest.
<table>
<tr>
<th align="center">
<img width="640" height="1px">
<p>
<small>
Documentation
</small>
</p>
</th>
<th align="center">
<img width="640" height="1">
<p>
<small>
Contribute
</small>
</p>
</th>
</tr>
<tr>
<td>
📚 [Getting started with Kairos](https://kairos.io/docs/getting-started) <br> :bulb: [Examples](https://kairos.io/docs/examples) <br> :movie_camera: [Video](https://kairos.io/docs/media/) <br> :open_hands:[Engage with the Community](https://kairos.io/community/)
</td>
<td>
🙌[ CONTRIBUTING.md ]( https://github.com/kairos-io/kairos/blob/master/CONTRIBUTING.md ) <br> :raising_hand: [ GOVERNANCE ]( https://github.com/kairos-io/kairos/blob/master/GOVERNANCE.md ) <br>:construction_worker:[Code of conduct](https://github.com/kairos-io/kairos/blob/master/CODE_OF_CONDUCT.md)
</td>
</tr>
</table>
| :exclamation: | This is experimental! |
|-|:-|
This is the Kairos kcrypt-challenger Kubernetes Native Extension.
## Usage
See the documentation in our website: https://kairos.io/docs/advanced/partition_encryption/.
## Installation
To install, use helm:
```
# Adds the kairos repo to helm
$ helm repo add kairos https://kairos-io.github.io/helm-charts
"kairos" has been added to your repositories
$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "kairos" chart repository
Update Complete. ⎈Happy Helming!⎈
# Install the CRD chart
$ helm install kairos-crd kairos/kairos-crds
NAME: kairos-crd
LAST DEPLOYED: Tue Sep 6 20:35:34 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
# Installs challenger
$ helm install kairos-challenger kairos/kcrypt-challenger
```

View File

@@ -0,0 +1,36 @@
/*
Copyright 2022.
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 v1alpha1 contains API Schema definitions for the keyserver v1alpha1 API group
// +kubebuilder:object:generate=true
// +groupName=keyserver.kairos.io
package v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var (
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "keyserver.kairos.io", Version: "v1alpha1"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)

View File

@@ -0,0 +1,77 @@
/*
Copyright 2022.
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 v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
// SealedVolumeSpec defines the desired state of SealedVolume
type SealedVolumeSpec struct {
TPMHash string `json:"TPMHash,omitempty"`
Partitions []PartitionSpec `json:"partitions,omitempty"`
Quarantined bool `json:"quarantined,omitempty"`
}
// PartitionSpec defines a Partition. A partition can be identified using
// any of the fields: Label, DeviceName, UUID. The Secret defines the secret
// which decrypts the partition.
type PartitionSpec struct {
Label string `json:"label,omitempty"`
DeviceName string `json:"deviceName,omitempty"`
UUID string `json:"uuid,omitempty"`
Secret *SecretSpec `json:"secret,omitempty"`
}
type SecretSpec struct {
Name string `json:"name,omitempty"`
Path string `json:"path,omitempty"`
}
// SealedVolumeStatus defines the observed state of SealedVolume
type SealedVolumeStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// SealedVolume is the Schema for the sealedvolumes API
type SealedVolume struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec SealedVolumeSpec `json:"spec,omitempty"`
Status SealedVolumeStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// SealedVolumeList contains a list of SealedVolume
type SealedVolumeList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []SealedVolume `json:"items"`
}
func init() {
SchemeBuilder.Register(&SealedVolume{}, &SealedVolumeList{})
}

View File

@@ -0,0 +1,157 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 2022.
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.
*/
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PartitionSpec) DeepCopyInto(out *PartitionSpec) {
*out = *in
if in.Secret != nil {
in, out := &in.Secret, &out.Secret
*out = new(SecretSpec)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PartitionSpec.
func (in *PartitionSpec) DeepCopy() *PartitionSpec {
if in == nil {
return nil
}
out := new(PartitionSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SealedVolume) DeepCopyInto(out *SealedVolume) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
out.Status = in.Status
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SealedVolume.
func (in *SealedVolume) DeepCopy() *SealedVolume {
if in == nil {
return nil
}
out := new(SealedVolume)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *SealedVolume) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SealedVolumeList) DeepCopyInto(out *SealedVolumeList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]SealedVolume, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SealedVolumeList.
func (in *SealedVolumeList) DeepCopy() *SealedVolumeList {
if in == nil {
return nil
}
out := new(SealedVolumeList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *SealedVolumeList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SealedVolumeSpec) DeepCopyInto(out *SealedVolumeSpec) {
*out = *in
if in.Partitions != nil {
in, out := &in.Partitions, &out.Partitions
*out = make([]PartitionSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SealedVolumeSpec.
func (in *SealedVolumeSpec) DeepCopy() *SealedVolumeSpec {
if in == nil {
return nil
}
out := new(SealedVolumeSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SealedVolumeStatus) DeepCopyInto(out *SealedVolumeStatus) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SealedVolumeStatus.
func (in *SealedVolumeStatus) DeepCopy() *SealedVolumeStatus {
if in == nil {
return nil
}
out := new(SealedVolumeStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretSpec) DeepCopyInto(out *SecretSpec) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretSpec.
func (in *SecretSpec) DeepCopy() *SecretSpec {
if in == nil {
return nil
}
out := new(SecretSpec)
in.DeepCopyInto(out)
return out
}

View File

@@ -0,0 +1,147 @@
package client
import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"time"
"github.com/jaypipes/ghw/pkg/block"
"github.com/kairos-io/kairos-challenger/pkg/constants"
"github.com/kairos-io/kairos-challenger/pkg/payload"
"github.com/kairos-io/kcrypt/pkg/bus"
"github.com/kairos-io/tpm-helpers"
"github.com/mudler/go-pluggable"
"github.com/mudler/yip/pkg/utils"
)
var errPartNotFound error = fmt.Errorf("pass for partition not found")
var errBadCertificate error = fmt.Errorf("unknown certificate")
func NewClient() (*Client, error) {
conf, err := unmarshalConfig()
if err != nil {
return nil, err
}
return &Client{Config: conf}, nil
}
// echo '{ "data": "{ \\"label\\": \\"LABEL\\" }"}' | sudo -E WSS_SERVER="http://localhost:8082/challenge" ./challenger "discovery.password"
func (c *Client) Start() error {
factory := pluggable.NewPluginFactory()
// Input: bus.EventInstallPayload
// Expected output: map[string]string{}
factory.Add(bus.EventDiscoveryPassword, func(e *pluggable.Event) pluggable.EventResponse {
b := &block.Partition{}
err := json.Unmarshal([]byte(e.Data), b)
if err != nil {
return pluggable.EventResponse{
Error: fmt.Sprintf("failed reading partitions: %s", err.Error()),
}
}
pass, err := c.waitPass(b, 30)
if err != nil {
return pluggable.EventResponse{
Error: fmt.Sprintf("failed getting pass: %s", err.Error()),
}
}
return pluggable.EventResponse{
Data: pass,
}
})
return factory.Run(pluggable.EventType(os.Args[1]), os.Stdin, os.Stdout)
}
func (c *Client) generatePass(postEndpoint string, p *block.Partition) error {
rand := utils.RandomString(32)
pass, err := tpm.EncryptBlob([]byte(rand))
if err != nil {
return err
}
bpass := base64.RawURLEncoding.EncodeToString(pass)
opts := []tpm.Option{
tpm.WithCAs([]byte(c.Config.Kcrypt.Challenger.Certificate)),
tpm.AppendCustomCAToSystemCA,
tpm.WithAdditionalHeader("label", p.FilesystemLabel),
tpm.WithAdditionalHeader("name", p.Name),
tpm.WithAdditionalHeader("uuid", p.UUID),
}
conn, err := tpm.Connection(postEndpoint, opts...)
if err != nil {
return err
}
return conn.WriteJSON(payload.Data{Passphrase: bpass, GeneratedBy: constants.TPMSecret})
}
func (c *Client) waitPass(p *block.Partition, attempts int) (pass string, err error) {
// IF we don't have any server configured, just do local
if c.Config.Kcrypt.Challenger.Server == "" {
return localPass(c.Config)
}
challengeEndpoint := fmt.Sprintf("%s/getPass", c.Config.Kcrypt.Challenger.Server)
postEndpoint := fmt.Sprintf("%s/postPass", c.Config.Kcrypt.Challenger.Server)
for tries := 0; tries < attempts; tries++ {
var generated bool
pass, generated, err = getPass(challengeEndpoint, c.Config.Kcrypt.Challenger.Certificate, p)
if err == errPartNotFound {
// IF server doesn't have a pass for us, then we generate one and we set it
err = c.generatePass(postEndpoint, p)
if err != nil {
return
}
// Attempt to fetch again - validate that the server has it now
tries = 0
continue
}
if generated { // passphrase is encrypted
return c.decryptPassphrase(pass)
}
if err == errBadCertificate { // No need to retry, won't succeed.
return
}
if err == nil { // passphrase available, no errors
return
}
fmt.Printf("Failed with error: %s . Will retry.\n", err.Error())
time.Sleep(1 * time.Second) // network errors? retry
}
return
}
// decryptPassphrase decodes (base64) and decrypts the passphrase returned
// by the challenger server.
func (c *Client) decryptPassphrase(pass string) (string, error) {
blob, err := base64.RawURLEncoding.DecodeString(pass)
if err != nil {
return "", err
}
// Decrypt and return it to unseal the LUKS volume
opts := []tpm.TPMOption{}
if c.Config.Kcrypt.Challenger.CIndex != "" {
opts = append(opts, tpm.WithIndex(c.Config.Kcrypt.Challenger.CIndex))
}
if c.Config.Kcrypt.Challenger.TPMDevice != "" {
opts = append(opts, tpm.WithDevice(c.Config.Kcrypt.Challenger.TPMDevice))
}
passBytes, err := tpm.DecryptBlob(blob, opts...)
return string(passBytes), err
}

View File

@@ -0,0 +1,49 @@
package client
import (
"github.com/kairos-io/kairos-sdk/collector"
kconfig "github.com/kairos-io/kcrypt/pkg/config"
"gopkg.in/yaml.v3"
)
type Client struct {
Config Config
}
type Config struct {
Kcrypt struct {
Challenger struct {
Server string `yaml:"challenger_server,omitempty"`
// Non-volatile index memory: where we store the encrypted passphrase (offline mode)
NVIndex string `yaml:"nv_index,omitempty"`
// Certificate index: this is where the rsa pair that decrypts the passphrase lives
CIndex string `yaml:"c_index,omitempty"`
TPMDevice string `yaml:"tpm_device,omitempty"`
Certificate string `yaml:"certificate,omitempty"`
}
}
}
func unmarshalConfig() (Config, error) {
var result Config
o := &collector.Options{NoLogs: true, MergeBootCMDLine: false}
if err := o.Apply(collector.Directories(append(kconfig.ConfigScanDirs, "/tmp/oem")...)); err != nil {
return result, err
}
c, err := collector.Scan(o, func(d []byte) ([]byte, error) {
return d, nil
})
if err != nil {
return result, err
}
a, _ := c.String()
err = yaml.Unmarshal([]byte(a), &result)
if err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,98 @@
package client
import (
"encoding/json"
"fmt"
"strings"
"github.com/kairos-io/kairos-challenger/pkg/constants"
"github.com/kairos-io/kairos-challenger/pkg/payload"
"github.com/jaypipes/ghw/pkg/block"
"github.com/kairos-io/tpm-helpers"
"github.com/mudler/yip/pkg/utils"
"github.com/pkg/errors"
)
const DefaultNVIndex = "0x1500000"
func getPass(server, certificate string, partition *block.Partition) (string, bool, error) {
msg, err := tpm.Get(server,
tpm.WithCAs([]byte(certificate)),
tpm.AppendCustomCAToSystemCA,
tpm.WithAdditionalHeader("label", partition.FilesystemLabel),
tpm.WithAdditionalHeader("name", partition.Name),
tpm.WithAdditionalHeader("uuid", partition.UUID))
if err != nil {
return "", false, err
}
result := payload.Data{}
err = json.Unmarshal(msg, &result)
if err != nil {
return "", false, errors.Wrap(err, string(msg))
}
if result.HasPassphrase() {
return fmt.Sprint(result.Passphrase), result.HasBeenGenerated() && result.GeneratedBy == constants.TPMSecret, nil
} else if result.HasError() {
if strings.Contains(result.Error, "No secret found for") {
return "", false, errPartNotFound
}
if strings.Contains(result.Error, "x509: certificate signed by unknown authority") {
return "", false, errBadCertificate
}
return "", false, fmt.Errorf(result.Error)
}
return "", false, errPartNotFound
}
func genAndStore(k Config) (string, error) {
opts := []tpm.TPMOption{}
if k.Kcrypt.Challenger.TPMDevice != "" {
opts = append(opts, tpm.WithDevice(k.Kcrypt.Challenger.TPMDevice))
}
if k.Kcrypt.Challenger.CIndex != "" {
opts = append(opts, tpm.WithIndex(k.Kcrypt.Challenger.CIndex))
}
// Generate a new one, and return it to luks
rand := utils.RandomString(32)
blob, err := tpm.EncryptBlob([]byte(rand))
if err != nil {
return "", err
}
nvindex := DefaultNVIndex
if k.Kcrypt.Challenger.NVIndex != "" {
nvindex = k.Kcrypt.Challenger.NVIndex
}
opts = append(opts, tpm.WithIndex(nvindex))
return rand, tpm.StoreBlob(blob, opts...)
}
func localPass(k Config) (string, error) {
index := DefaultNVIndex
if k.Kcrypt.Challenger.NVIndex != "" {
index = k.Kcrypt.Challenger.NVIndex
}
opts := []tpm.TPMOption{tpm.WithIndex(index)}
if k.Kcrypt.Challenger.TPMDevice != "" {
opts = append(opts, tpm.WithDevice(k.Kcrypt.Challenger.TPMDevice))
}
encodedPass, err := tpm.ReadBlob(opts...)
if err != nil {
// Generate if we fail to read from the assigned blob
return genAndStore(k)
}
// Decode and give it back
opts = []tpm.TPMOption{}
if k.Kcrypt.Challenger.CIndex != "" {
opts = append(opts, tpm.WithIndex(k.Kcrypt.Challenger.CIndex))
}
if k.Kcrypt.Challenger.TPMDevice != "" {
opts = append(opts, tpm.WithDevice(k.Kcrypt.Challenger.TPMDevice))
}
pass, err := tpm.DecryptBlob(encodedPass, opts...)
return string(pass), err
}

30
cmd/discovery/main.go Normal file
View File

@@ -0,0 +1,30 @@
package main
import (
"fmt"
"os"
"github.com/kairos-io/kairos-challenger/cmd/discovery/client"
"github.com/kairos-io/kcrypt/pkg/bus"
"github.com/kairos-io/tpm-helpers"
)
func main() {
if len(os.Args) >= 2 && bus.IsEventDefined(os.Args[1]) {
c, err := client.NewClient()
checkErr(err)
checkErr(c.Start())
return
}
pubhash, err := tpm.GetPubHash()
checkErr(err)
fmt.Print(pubhash)
}
func checkErr(err error) {
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}

View File

@@ -0,0 +1,71 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.9.2
creationTimestamp: null
name: sealedvolumes.keyserver.kairos.io
spec:
group: keyserver.kairos.io
names:
kind: SealedVolume
listKind: SealedVolumeList
plural: sealedvolumes
singular: sealedvolume
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: SealedVolume is the Schema for the sealedvolumes API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: SealedVolumeSpec defines the desired state of SealedVolume
properties:
TPMHash:
type: string
partitions:
items:
description: 'PartitionSpec defines a Partition. A partition can
be identified using any of the fields: Label, DeviceName, UUID.
The Secret defines the secret which decrypts the partition.'
properties:
deviceName:
type: string
label:
type: string
secret:
properties:
name:
type: string
path:
type: string
type: object
uuid:
type: string
type: object
type: array
quarantined:
type: boolean
type: object
status:
description: SealedVolumeStatus defines the observed state of SealedVolume
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -0,0 +1,21 @@
# This kustomization.yaml is not intended to be run by itself,
# since it depends on service name and namespace that are out of this kustomize package.
# It should be run by config/default
resources:
- bases/keyserver.kairos.io_sealedvolumes.yaml
#+kubebuilder:scaffold:crdkustomizeresource
patchesStrategicMerge:
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
# patches here are for enabling the conversion webhook for each CRD
#- patches/webhook_in_sealedvolumes.yaml
#+kubebuilder:scaffold:crdkustomizewebhookpatch
# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
# patches here are for enabling the CA injection for each CRD
#- patches/cainjection_in_sealedvolumes.yaml
#+kubebuilder:scaffold:crdkustomizecainjectionpatch
# the following config is for teaching kustomize how to do kustomization for CRDs.
configurations:
- kustomizeconfig.yaml

View File

@@ -0,0 +1,19 @@
# This file is for teaching kustomize how to substitute name and namespace reference in CRD
nameReference:
- kind: Service
version: v1
fieldSpecs:
- kind: CustomResourceDefinition
version: v1
group: apiextensions.k8s.io
path: spec/conversion/webhook/clientConfig/service/name
namespace:
- kind: CustomResourceDefinition
version: v1
group: apiextensions.k8s.io
path: spec/conversion/webhook/clientConfig/service/namespace
create: false
varReference:
- path: metadata/annotations

View File

@@ -0,0 +1,7 @@
# The following patch adds a directive for certmanager to inject CA into the CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
name: sealedvolumes.keyserver.kairos.io

View File

@@ -0,0 +1,16 @@
# The following patch enables a conversion webhook for the CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: sealedvolumes.keyserver.kairos.io
spec:
conversion:
strategy: Webhook
webhook:
clientConfig:
service:
namespace: system
name: webhook-service
path: /convert
conversionReviewVersions:
- v1

View File

@@ -0,0 +1,74 @@
# Adds namespace to all resources.
namespace: kcrypt-controller-system
# Value of this field is prepended to the
# names of all resources, e.g. a deployment named
# "wordpress" becomes "alices-wordpress".
# Note that it should also match with the prefix (text before '-') of the namespace
# field above.
namePrefix: kcrypt-controller-
# Labels to add to all resources and selectors.
#commonLabels:
# someName: someValue
bases:
- ../crd
- ../rbac
- ../manager
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
#- ../webhook
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
#- ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
#- ../prometheus
patchesStrategicMerge:
# Protect the /metrics endpoint by putting it behind auth.
# If you want your controller-manager to expose the /metrics
# endpoint w/o any authn/z, please comment the following line.
- manager_auth_proxy_patch.yaml
# Mount the controller config file for loading manager configurations
# through a ComponentConfig type
#- manager_config_patch.yaml
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
#- manager_webhook_patch.yaml
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
# 'CERTMANAGER' needs to be enabled to use ca injection
#- webhookcainjection_patch.yaml
# the following config is for teaching kustomize how to do var substitution
vars:
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
# objref:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert # this name should match the one in certificate.yaml
# fieldref:
# fieldpath: metadata.namespace
#- name: CERTIFICATE_NAME
# objref:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert # this name should match the one in certificate.yaml
#- name: SERVICE_NAMESPACE # namespace of the service
# objref:
# kind: Service
# version: v1
# name: webhook-service
# fieldref:
# fieldpath: metadata.namespace
#- name: SERVICE_NAME
# objref:
# kind: Service
# version: v1
# name: webhook-service

View File

@@ -0,0 +1,39 @@
# This patch inject a sidecar container which is a HTTP proxy for the
# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews.
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
spec:
containers:
- name: kube-rbac-proxy
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.0
args:
- "--secure-listen-address=0.0.0.0:8443"
- "--upstream=http://127.0.0.1:8080/"
- "--logtostderr=true"
- "--v=0"
ports:
- containerPort: 8443
protocol: TCP
name: https
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 5m
memory: 64Mi
- name: manager
args:
- "--health-probe-bind-address=:8081"
- "--metrics-bind-address=127.0.0.1:8080"
- "--leader-elect"

View File

@@ -0,0 +1,20 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
spec:
containers:
- name: manager
args:
- "--config=controller_manager_config.yaml"
volumeMounts:
- name: manager-config
mountPath: /controller_manager_config.yaml
subPath: controller_manager_config.yaml
volumes:
- name: manager-config
configMap:
name: manager-config

View File

@@ -0,0 +1,74 @@
# Adds namespace to all resources.
namespace: kcrypt-controller-system
# Value of this field is prepended to the
# names of all resources, e.g. a deployment named
# "wordpress" becomes "alices-wordpress".
# Note that it should also match with the prefix (text before '-') of the namespace
# field above.
namePrefix: kcrypt-controller-
# Labels to add to all resources and selectors.
#commonLabels:
# someName: someValue
bases:
- ../crd
- ../rbac
- ../manager
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
#- ../webhook
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
#- ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
#- ../prometheus
patchesStrategicMerge:
# Protect the /metrics endpoint by putting it behind auth.
# If you want your controller-manager to expose the /metrics
# endpoint w/o any authn/z, please comment the following line.
- manager_auth_proxy_patch.yaml
- pull.yaml
# Mount the controller config file for loading manager configurations
# through a ComponentConfig type
#- manager_config_patch.yaml
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
#- manager_webhook_patch.yaml
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
# 'CERTMANAGER' needs to be enabled to use ca injection
#- webhookcainjection_patch.yaml
# the following config is for teaching kustomize how to do var substitution
vars:
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
# objref:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert # this name should match the one in certificate.yaml
# fieldref:
# fieldpath: metadata.namespace
#- name: CERTIFICATE_NAME
# objref:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert # this name should match the one in certificate.yaml
#- name: SERVICE_NAMESPACE # namespace of the service
# objref:
# kind: Service
# version: v1
# name: webhook-service
# fieldref:
# fieldpath: metadata.namespace
#- name: SERVICE_NAME
# objref:
# kind: Service
# version: v1
# name: webhook-service

View File

@@ -0,0 +1,39 @@
# This patch inject a sidecar container which is a HTTP proxy for the
# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews.
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
spec:
containers:
- name: kube-rbac-proxy
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.0
args:
- "--secure-listen-address=0.0.0.0:8443"
- "--upstream=http://127.0.0.1:8080/"
- "--logtostderr=true"
- "--v=0"
ports:
- containerPort: 8443
protocol: TCP
name: https
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 5m
memory: 64Mi
- name: manager
args:
- "--health-probe-bind-address=:8081"
- "--metrics-bind-address=127.0.0.1:8080"
- "--leader-elect"

View File

@@ -0,0 +1,21 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
spec:
containers:
- name: manager
imagePullPolicy: IfNotPresent
args:
- "--config=controller_manager_config.yaml"
volumeMounts:
- name: manager-config
mountPath: /controller_manager_config.yaml
subPath: controller_manager_config.yaml
volumes:
- name: manager-config
configMap:
name: manager-config

12
config/dev/pull.yaml Normal file
View File

@@ -0,0 +1,12 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
spec:
containers:
- name: manager
imagePullPolicy: IfNotPresent

View File

@@ -0,0 +1,21 @@
apiVersion: controller-runtime.sigs.k8s.io/v1alpha1
kind: ControllerManagerConfig
health:
healthProbeBindAddress: :8081
metrics:
bindAddress: 127.0.0.1:8080
webhook:
port: 9443
leaderElection:
leaderElect: true
resourceName: 8fc84668.kairos.io
# leaderElectionReleaseOnCancel defines if the leader should step down volume
# when the Manager ends. This requires the binary to immediately end when the
# Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
# speeds up voluntary leader transitions as the new leader don't have to wait
# LeaseDuration time first.
# In the default scaffold provided, the program ends immediately after
# the manager stops, so would be fine to enable this option. However,
# if you are doing or is intended to do any operation such as perform cleanups
# after the manager stops then its usage might be unsafe.
# leaderElectionReleaseOnCancel: true

View File

@@ -0,0 +1,16 @@
resources:
- manager.yaml
generatorOptions:
disableNameSuffixHash: true
configMapGenerator:
- files:
- controller_manager_config.yaml
name: manager-config
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: controller
newName: controller
newTag: latest

View File

@@ -0,0 +1,85 @@
apiVersion: v1
kind: Namespace
metadata:
labels:
control-plane: controller-manager
name: system
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
labels:
control-plane: controller-manager
spec:
selector:
matchLabels:
control-plane: controller-manager
replicas: 1
template:
metadata:
annotations:
kubectl.kubernetes.io/default-container: manager
labels:
control-plane: controller-manager
spec:
securityContext:
runAsNonRoot: true
# TODO(user): For common cases that do not require escalating privileges
# it is recommended to ensure that all your Pods/Containers are restrictive.
# More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
# Please uncomment the following code if your project does NOT have to work on old Kubernetes
# versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ).
# seccompProfile:
# type: RuntimeDefault
containers:
- command:
- /manager
args:
- --leader-elect
image: controller:latest
name: manager
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
livenessProbe:
httpGet:
path: /healthz
port: 8081
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /readyz
port: 8081
initialDelaySeconds: 5
periodSeconds: 10
# TODO(user): Configure the resources accordingly based on the project requirements.
# More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 10m
memory: 64Mi
serviceAccountName: controller-manager
terminationGracePeriodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
labels:
control-plane: controller-manager
name: kcrypt-escrow-server
namespace: system
spec:
type: NodePort
ports:
- name: wss
port: 8082
protocol: TCP
targetPort: wss

View File

@@ -0,0 +1,27 @@
# These resources constitute the fully configured set of manifests
# used to generate the 'manifests/' directory in a bundle.
resources:
- bases/kcrypt-controller.clusterserviceversion.yaml
- ../default
- ../samples
- ../scorecard
# [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix.
# Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager.
# These patches remove the unnecessary "cert" volume and its manager container volumeMount.
#patchesJson6902:
#- target:
# group: apps
# version: v1
# kind: Deployment
# name: controller-manager
# namespace: system
# patch: |-
# # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs.
# # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment.
# - op: remove
# path: /spec/template/spec/containers/1/volumeMounts/0
# # Remove the "cert" volume, since OLM will create and mount a set of certs.
# # Update the indices in this path if adding or removing volumes in the manager's Deployment.
# - op: remove
# path: /spec/template/spec/volumes/0

View File

@@ -0,0 +1,2 @@
resources:
- monitor.yaml

View File

@@ -0,0 +1,20 @@
# Prometheus Monitor Service (Metrics)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
control-plane: controller-manager
name: controller-manager-metrics-monitor
namespace: system
spec:
endpoints:
- path: /metrics
port: https
scheme: https
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
tlsConfig:
insecureSkipVerify: true
selector:
matchLabels:
control-plane: controller-manager

View File

@@ -0,0 +1,9 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: metrics-reader
rules:
- nonResourceURLs:
- "/metrics"
verbs:
- get

View File

@@ -0,0 +1,17 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: proxy-role
rules:
- apiGroups:
- authentication.k8s.io
resources:
- tokenreviews
verbs:
- create
- apiGroups:
- authorization.k8s.io
resources:
- subjectaccessreviews
verbs:
- create

View File

@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: proxy-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: proxy-role
subjects:
- kind: ServiceAccount
name: controller-manager
namespace: system

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
labels:
control-plane: controller-manager
name: controller-manager-metrics-service
namespace: system
spec:
ports:
- name: https
port: 8443
protocol: TCP
targetPort: https
selector:
control-plane: controller-manager

View File

@@ -0,0 +1,18 @@
resources:
# All RBAC will be applied under this service account in
# the deployment namespace. You may comment out this resource
# if your manager will use a service account that exists at
# runtime. Be sure to update RoleBinding and ClusterRoleBinding
# subjects if changing service account names.
- service_account.yaml
- role.yaml
- role_binding.yaml
- leader_election_role.yaml
- leader_election_role_binding.yaml
# Comment the following 4 lines if you want to disable
# the auth proxy (https://github.com/brancz/kube-rbac-proxy)
# which protects your /metrics endpoint.
- auth_proxy_service.yaml
- auth_proxy_role.yaml
- auth_proxy_role_binding.yaml
- auth_proxy_client_clusterrole.yaml

View File

@@ -0,0 +1,37 @@
# permissions to do leader election.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: leader-election-role
rules:
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch

View File

@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: leader-election-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: leader-election-role
subjects:
- kind: ServiceAccount
name: controller-manager
namespace: system

42
config/rbac/role.yaml Normal file
View File

@@ -0,0 +1,42 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
creationTimestamp: null
name: manager-role
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- create
- get
- list
- watch
- apiGroups:
- keyserver.kairos.io
resources:
- sealedvolumes
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- keyserver.kairos.io
resources:
- sealedvolumes/finalizers
verbs:
- update
- apiGroups:
- keyserver.kairos.io
resources:
- sealedvolumes/status
verbs:
- get
- patch
- update

View File

@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: manager-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: manager-role
subjects:
- kind: ServiceAccount
name: controller-manager
namespace: system

View File

@@ -0,0 +1,24 @@
# permissions for end users to edit sealedvolumes.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: sealedvolume-editor-role
rules:
- apiGroups:
- keyserver.kairos.io
resources:
- sealedvolumes
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- keyserver.kairos.io
resources:
- sealedvolumes/status
verbs:
- get

View File

@@ -0,0 +1,20 @@
# permissions for end users to view sealedvolumes.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: sealedvolume-viewer-role
rules:
- apiGroups:
- keyserver.kairos.io
resources:
- sealedvolumes
verbs:
- get
- list
- watch
- apiGroups:
- keyserver.kairos.io
resources:
- sealedvolumes/status
verbs:
- get

View File

@@ -0,0 +1,5 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: controller-manager
namespace: system

View File

@@ -0,0 +1,6 @@
apiVersion: keyserver.kairos.io/v1alpha1
kind: SealedVolume
metadata:
name: sealedvolume-sample
spec:
# TODO(user): Add fields here

View File

@@ -0,0 +1,4 @@
## Append samples you want in your CSV to this file as resources ##
resources:
- keyserver_v1alpha1_sealedvolume.yaml
#+kubebuilder:scaffold:manifestskustomizesamples

View File

@@ -0,0 +1,7 @@
apiVersion: scorecard.operatorframework.io/v1alpha3
kind: Configuration
metadata:
name: config
stages:
- parallel: true
tests: []

View File

@@ -0,0 +1,16 @@
resources:
- bases/config.yaml
patchesJson6902:
- path: patches/basic.config.yaml
target:
group: scorecard.operatorframework.io
version: v1alpha3
kind: Configuration
name: config
- path: patches/olm.config.yaml
target:
group: scorecard.operatorframework.io
version: v1alpha3
kind: Configuration
name: config
#+kubebuilder:scaffold:patchesJson6902

View File

@@ -0,0 +1,10 @@
- op: add
path: /stages/0/tests/-
value:
entrypoint:
- scorecard-test
- basic-check-spec
image: quay.io/operator-framework/scorecard-test:v1.23.0
labels:
suite: basic
test: basic-check-spec-test

View File

@@ -0,0 +1,50 @@
- op: add
path: /stages/0/tests/-
value:
entrypoint:
- scorecard-test
- olm-bundle-validation
image: quay.io/operator-framework/scorecard-test:v1.23.0
labels:
suite: olm
test: olm-bundle-validation-test
- op: add
path: /stages/0/tests/-
value:
entrypoint:
- scorecard-test
- olm-crds-have-validation
image: quay.io/operator-framework/scorecard-test:v1.23.0
labels:
suite: olm
test: olm-crds-have-validation-test
- op: add
path: /stages/0/tests/-
value:
entrypoint:
- scorecard-test
- olm-crds-have-resources
image: quay.io/operator-framework/scorecard-test:v1.23.0
labels:
suite: olm
test: olm-crds-have-resources-test
- op: add
path: /stages/0/tests/-
value:
entrypoint:
- scorecard-test
- olm-spec-descriptors
image: quay.io/operator-framework/scorecard-test:v1.23.0
labels:
suite: olm
test: olm-spec-descriptors-test
- op: add
path: /stages/0/tests/-
value:
entrypoint:
- scorecard-test
- olm-status-descriptors
image: quay.io/operator-framework/scorecard-test:v1.23.0
labels:
suite: olm
test: olm-status-descriptors-test

View File

@@ -0,0 +1,63 @@
/*
Copyright 2022.
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 controllers
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
keyserverv1alpha1 "github.com/kairos-io/kairos-challenger/api/v1alpha1"
)
// SealedVolumeReconciler reconciles a SealedVolume object
type SealedVolumeReconciler struct {
client.Client
Scheme *runtime.Scheme
}
//+kubebuilder:rbac:groups=keyserver.kairos.io,resources=sealedvolumes,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=keyserver.kairos.io,resources=sealedvolumes/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=keyserver.kairos.io,resources=sealedvolumes/finalizers,verbs=update
//+kubebuilder:rbac:groups="",resources=secrets,verbs=create;get;list;watch
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the SealedVolume object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile
func (r *SealedVolumeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = log.FromContext(ctx)
// TODO(user): your logic here
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *SealedVolumeReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&keyserverv1alpha1.SealedVolume{}).
Complete(r)
}

79
controllers/suite_test.go Normal file
View File

@@ -0,0 +1,79 @@
/*
Copyright 2022.
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 controllers
import (
"path/filepath"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
keyserverv1alpha1 "github.com/kairos-io/kairos-challenger/api/v1alpha1"
//+kubebuilder:scaffold:imports
)
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Control")
}
var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
ErrorIfCRDPathMissing: true,
}
var err error
// cfg is defined in this file globally.
cfg, err = testEnv.Start()
Expect(err).NotTo(HaveOccurred())
Expect(cfg).NotTo(BeNil())
err = keyserverv1alpha1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
//+kubebuilder:scaffold:scheme
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient).NotTo(BeNil())
}, 60)
var _ = AfterSuite(func() {
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).NotTo(HaveOccurred())
})

3
earthly.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
docker run --privileged -v /var/run/docker.sock:/var/run/docker.sock --rm -t -v $(pwd):/workspace -v earthly-tmp:/tmp/earthly:rw earthly/earthly:v0.7.8 --allow-privileged $@

View File

@@ -0,0 +1,22 @@
# echo '{ "data": "{ \\"label\\": \\"COS_PERSISTENT\\" }"}' | sudo -E WSS_SERVER="http://localhost:8082/challenge" ./challenger "discovery.password"
apiVersion: v1
kind: Secret
metadata:
name: mysecret
namespace: default
type: Opaque
stringData:
pass: "awesome-passphrase"
---
apiVersion: keyserver.kairos.io/v1alpha1
kind: SealedVolume
metadata:
name: test
namespace: default
spec:
TPMHash: "something"
partitionSecrets:
LABEL:
name: mysecret
path: pass
quarantined: false

161
go.mod Normal file
View File

@@ -0,0 +1,161 @@
module github.com/kairos-io/kairos-challenger
go 1.20
require (
github.com/go-logr/logr v1.2.4
github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.5.0
github.com/jaypipes/ghw v0.11.0
github.com/kairos-io/kairos-sdk v0.0.8
github.com/kairos-io/kcrypt v0.7.0
github.com/kairos-io/tpm-helpers v0.0.0-20230119140150-3fa97128ef6b
github.com/mudler/go-pluggable v0.0.0-20230126220627-7710299a0ae5
github.com/mudler/go-processmanager v0.0.0-20220724164624-c45b5c61312d
github.com/mudler/yip v1.2.0
github.com/onsi/ginkgo/v2 v2.10.0
github.com/onsi/gomega v1.27.8
github.com/pkg/errors v0.9.1
github.com/spectrocloud/peg v0.0.0-20230407121159-2e15270c4a46
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.27.2
k8s.io/apimachinery v0.27.2
k8s.io/client-go v0.27.2
sigs.k8s.io/controller-runtime v0.15.0
)
require (
atomicgo.dev/cursor v0.1.1 // indirect
atomicgo.dev/keyboard v0.2.9 // indirect
atomicgo.dev/schedule v0.0.2 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/hcsshim v0.10.0-rc.8 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/avast/retry-go v3.0.0+incompatible // indirect
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bramvdbogaerde/go-scp v1.2.1 // indirect
github.com/cavaliergopher/grab/v3 v3.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 // indirect
github.com/codingsince1985/checksum v1.2.6 // indirect
github.com/containerd/cgroups v1.1.0 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/containerd/containerd v1.7.1 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denisbrodbeck/machineid v1.0.1 // indirect
github.com/docker/cli v23.0.5+incompatible // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker v23.0.5+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emicklei/go-restful/v3 v3.10.1 // indirect
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/folbricht/tpmk v0.1.2-0.20230104073416-f20b20c289d7 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-logr/zapr v1.2.4 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.1 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/certificate-transparency-go v1.1.4 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-attestation v0.4.4-0.20220404204839-8820d49b18d9 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/go-containerregistry v0.15.2 // indirect
github.com/google/go-tpm v0.3.3 // indirect
github.com/google/go-tpm-tools v0.3.10 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gookit/color v1.5.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.15 // indirect
github.com/ipfs/go-log v1.0.5 // indirect
github.com/ipfs/go-log/v2 v2.5.1 // indirect
github.com/itchyny/gojq v0.12.12 // indirect
github.com/itchyny/timefmt-go v0.1.5 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/lithammer/fuzzysearch v1.1.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc3 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 // indirect
github.com/prometheus/client_golang v1.15.1 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/pterm/pterm v0.12.61 // indirect
github.com/qeesung/image2ascii v1.0.1 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/twpayne/go-vfs v1.7.2 // indirect
github.com/vbatts/tar-split v0.11.3 // indirect
github.com/wayneashleyberry/terminal-dimensions v1.1.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opencensus.io v0.24.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/oauth2 v0.7.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/term v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.9.3 // indirect
gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230323212658-478b75c54725 // indirect
google.golang.org/grpc v1.54.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
howett.net/plist v1.0.0 // indirect
k8s.io/apiextensions-apiserver v0.27.2 // indirect
k8s.io/component-base v0.27.2 // indirect
k8s.io/klog/v2 v2.90.1 // indirect
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

1157
go.sum Normal file

File diff suppressed because it is too large Load Diff

15
hack/boilerplate.go.txt Normal file
View File

@@ -0,0 +1,15 @@
/*
Copyright 2022.
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.
*/

133
main.go Normal file
View File

@@ -0,0 +1,133 @@
/*
Copyright 2022.
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 main
import (
"context"
"flag"
"os"
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
"k8s.io/client-go/kubernetes"
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
keyserverv1alpha1 "github.com/kairos-io/kairos-challenger/api/v1alpha1"
"github.com/kairos-io/kairos-challenger/controllers"
"github.com/kairos-io/kairos-challenger/pkg/challenger"
//+kubebuilder:scaffold:imports
)
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(keyserverv1alpha1.AddToScheme(scheme))
//+kubebuilder:scaffold:scheme
}
func main() {
var metricsAddr string
var enableLeaderElection bool
var probeAddr string
var challengerAddr string
var namespace string
flag.StringVar(&namespace, "namespace", "kcrypt", "The namespace the challenger will read secrets from.")
flag.StringVar(&challengerAddr, "challenger-bind-address", ":8082", "The address the challenger server endpoint binds to.")
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
opts := zap.Options{
Development: true,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,
Port: 9443,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "8fc84668.kairos.io",
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
// when the Manager ends. This requires the binary to immediately end when the
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
// speeds up voluntary leader transitions as the new leader don't have to wait
// LeaseDuration time first.
//
// In the default scaffold provided, the program ends immediately after
// the manager stops, so would be fine to enable this option. However,
// if you are doing or is intended to do any operation such as perform cleanups
// after the manager stops then its usage might be unsafe.
// LeaderElectionReleaseOnCancel: true,
})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
reconciler := &controllers.SealedVolumeReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}
if err = reconciler.SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "SealedVolume")
os.Exit(1)
}
//+kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up health check")
os.Exit(1)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up ready check")
os.Exit(1)
}
clientset, err := kubernetes.NewForConfig(mgr.GetConfig())
if err != nil {
setupLog.Error(err, "unable to get clientset")
os.Exit(1)
}
serverLog := ctrl.Log.WithName("server")
go challenger.Start(context.Background(), serverLog, clientset, reconciler, namespace, challengerAddr)
setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}

View File

@@ -0,0 +1,395 @@
package challenger
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/go-logr/logr"
keyserverv1alpha1 "github.com/kairos-io/kairos-challenger/api/v1alpha1"
"github.com/kairos-io/kairos-challenger/pkg/constants"
"github.com/kairos-io/kairos-challenger/pkg/payload"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/kairos-io/kairos-challenger/controllers"
tpm "github.com/kairos-io/tpm-helpers"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"github.com/gorilla/websocket"
)
// PassphraseRequestData is a struct that holds all the information needed in
// order to lookup a passphrase for a specific tpm hash.
type PassphraseRequestData struct {
TPMHash string
Label string
DeviceName string
UUID string
}
type SealedVolumeData struct {
Quarantined bool
SecretName string
SecretPath string
PartitionLabel string
VolumeName string
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
func cleanKubeName(s string) (d string) {
d = strings.ReplaceAll(s, "_", "-")
d = strings.ToLower(d)
return
}
func (s SealedVolumeData) DefaultSecret() (string, string) {
secretName := fmt.Sprintf("%s-%s", s.VolumeName, s.PartitionLabel)
secretPath := "passphrase"
if s.SecretName != "" {
secretName = s.SecretName
}
if s.SecretPath != "" {
secretPath = s.SecretPath
}
return cleanKubeName(secretName), cleanKubeName(secretPath)
}
func writeRead(conn *websocket.Conn, input []byte) ([]byte, error) {
writer, err := conn.NextWriter(websocket.BinaryMessage)
if err != nil {
return nil, err
}
if _, err := writer.Write(input); err != nil {
return nil, err
}
if err := writer.Close(); err != nil {
return nil, err
}
_, reader, err := conn.NextReader()
if err != nil {
return nil, err
}
return ioutil.ReadAll(reader)
}
func getPubHash(token string) (string, error) {
ek, _, err := tpm.GetAttestationData(token)
if err != nil {
return "", err
}
return tpm.DecodePubHash(ek)
}
func Start(ctx context.Context, logger logr.Logger, kclient *kubernetes.Clientset, reconciler *controllers.SealedVolumeReconciler, namespace, address string) {
logger.Info("Challenger started", "address", address)
s := http.Server{
Addr: address,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
m := http.NewServeMux()
m.HandleFunc("/postPass", func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
logger.Error(err, "upgrading connection")
return
}
defer func() {
err := conn.Close()
if err != nil {
logger.Error(err, "closing the connection")
}
}()
logger.Info("Receiving passphrase")
if err := tpm.AuthRequest(r, conn); err != nil {
errorMessage(conn, logger, err, "auth request")
return
}
logger.Info("[Receiving passphrase] auth succeeded")
token := r.Header.Get("Authorization")
hashEncoded, err := getPubHash(token)
if err != nil {
errorMessage(conn, logger, err, "decoding pubhash")
return
}
logger.Info("[Receiving passphrase] pubhash", "encodedhash", hashEncoded)
label := r.Header.Get("label")
name := r.Header.Get("name")
uuid := r.Header.Get("uuid")
v := &payload.Data{}
logger.Info("Reading request data", "label", label, "name", name, "uuid", uuid)
volumeList := &keyserverv1alpha1.SealedVolumeList{}
for {
if err := reconciler.List(ctx, volumeList, &client.ListOptions{Namespace: namespace}); err != nil {
logger.Error(err, "listing volumes")
continue
}
break
}
logger.Info("Looking up volume with request data")
sealedVolumeData := findVolumeFor(PassphraseRequestData{
TPMHash: hashEncoded,
Label: label,
DeviceName: name,
UUID: uuid,
}, volumeList)
if sealedVolumeData == nil {
errorMessage(conn, logger, fmt.Errorf("no TPM Hash found for %s", hashEncoded), "")
return
}
logger.Info("[Looking up volume with request data] succeeded")
if err := conn.ReadJSON(v); err != nil {
logger.Error(err, "reading json from connection")
return
}
if !v.HasPassphrase() {
errorMessage(conn, logger, fmt.Errorf("invalid answer from client: doesn't contain any passphrase"), "")
}
if v.HasError() {
errorMessage(conn, logger, fmt.Errorf("error: %s", v.Error), v.Error)
}
secretName, secretPath := sealedVolumeData.DefaultSecret()
logger.Info("Looking up secret in with name", "name", secretName, "namespace", namespace)
_, err = kclient.CoreV1().Secrets(namespace).Get(ctx, secretName, v1.GetOptions{})
if err == nil {
logger.Info("Posted for already existing secret - ignoring")
return
}
if !apierrors.IsNotFound(err) {
errorMessage(conn, logger, err, "failed getting secret")
return
}
logger.Info("secret not found, creating one")
secret := corev1.Secret{
TypeMeta: v1.TypeMeta{
Kind: "Secret",
APIVersion: "apps/v1",
},
ObjectMeta: v1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
StringData: map[string]string{
secretPath: v.Passphrase,
constants.GeneratedByKey: v.GeneratedBy,
},
Type: "Opaque",
}
_, err = kclient.CoreV1().Secrets(namespace).Create(ctx, &secret, v1.CreateOptions{})
if err != nil {
errorMessage(conn, logger, err, "failed during secret creation")
}
logger.Info("created new secret")
})
m.HandleFunc("/getPass", func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
logger.Error(err, "upgrading connection")
return
}
defer func() {
err := conn.Close()
if err != nil {
logger.Error(err, "closing the connection")
}
}()
logger.Info("Received connection")
volumeList := &keyserverv1alpha1.SealedVolumeList{}
for {
if err := reconciler.List(ctx, volumeList, &client.ListOptions{Namespace: namespace}); err != nil {
logger.Error(err, "listing volumes")
continue
}
break
}
logger.Info("reading data from request")
token := r.Header.Get("Authorization")
label := r.Header.Get("label")
name := r.Header.Get("name")
uuid := r.Header.Get("uuid")
tokenStr := "empty"
if token != "" {
tokenStr = "not empty"
}
logger.Info("request data", "token", tokenStr, "label", label, "name", name, "uuid", uuid)
if err := tpm.AuthRequest(r, conn); err != nil {
logger.Error(err, "error validating challenge")
return
}
hashEncoded, err := getPubHash(token)
if err != nil {
logger.Error(err, "error decoding pubhash")
return
}
logger.Info("Looking up volume with request data")
sealedVolumeData := findVolumeFor(PassphraseRequestData{
TPMHash: hashEncoded,
Label: label,
DeviceName: name,
UUID: uuid,
}, volumeList)
if sealedVolumeData == nil {
errorMessage(conn, logger, fmt.Errorf("no volume found with data from request and hash: %s", hashEncoded), "")
return
}
logger.Info("[Looking up volume with request data] succeeded")
if sealedVolumeData.Quarantined {
errorMessage(conn, logger, fmt.Errorf("quarantined: %s", sealedVolumeData.PartitionLabel), "")
return
}
secretName, secretPath := sealedVolumeData.DefaultSecret()
// 1. The admin sets a specific cleartext password from Kube manager
// SealedVolume -> with a secret .
// 2. The admin just adds a SealedVolume associated with a TPM Hash ( you don't provide any passphrase )
// 3. There is no challenger server at all (offline mode)
//
logger.Info(fmt.Sprintf("looking up secret %s in namespace %s", secretName, namespace))
secret, err := kclient.CoreV1().Secrets(namespace).Get(ctx, secretName, v1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
errorMessage(conn, logger, fmt.Errorf("No secret found for %s and %s", hashEncoded, sealedVolumeData.PartitionLabel), "")
} else {
errorMessage(conn, logger, err, "getting the secret from Kubernetes")
}
return
}
logger.Info(fmt.Sprintf("secret %s found in namespace %s", secretName, namespace))
passphrase := secret.Data[secretPath]
generatedBy := secret.Data[constants.GeneratedByKey]
writer, err := conn.NextWriter(websocket.BinaryMessage)
if err != nil {
logger.Error(err, "getting a writer from the connection")
}
p := payload.Data{Passphrase: string(passphrase), GeneratedBy: string(generatedBy)}
err = json.NewEncoder(writer).Encode(p)
if err != nil {
logger.Error(err, "writing passphrase to the websocket channel")
}
if err = writer.Close(); err != nil {
logger.Error(err, "closing the writer")
return
}
})
s.Handler = logRequestHandler(logger, m)
go func() {
err := s.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
panic(err)
}
}()
go func() {
<-ctx.Done()
s.Shutdown(ctx)
}()
}
func findVolumeFor(requestData PassphraseRequestData, volumeList *keyserverv1alpha1.SealedVolumeList) *SealedVolumeData {
for _, v := range volumeList.Items {
if requestData.TPMHash == v.Spec.TPMHash {
for _, p := range v.Spec.Partitions {
deviceNameMatches := requestData.DeviceName != "" && p.DeviceName == requestData.DeviceName
uuidMatches := requestData.UUID != "" && p.UUID == requestData.UUID
labelMatches := requestData.Label != "" && p.Label == requestData.Label
secretName := ""
if p.Secret != nil && p.Secret.Name != "" {
secretName = p.Secret.Name
}
secretPath := ""
if p.Secret != nil && p.Secret.Path != "" {
secretPath = p.Secret.Path
}
if labelMatches || uuidMatches || deviceNameMatches {
return &SealedVolumeData{
Quarantined: v.Spec.Quarantined,
SecretName: secretName,
SecretPath: secretPath,
VolumeName: v.Name,
PartitionLabel: p.Label,
}
}
}
}
}
return nil
}
// errorMessage should be used when an error should be both, printed to the stdout
// and sent over the wire to the websocket client.
func errorMessage(conn *websocket.Conn, logger logr.Logger, theErr error, description string) {
if theErr == nil {
return
}
logger.Error(theErr, description)
writer, err := conn.NextWriter(websocket.BinaryMessage)
if err != nil {
logger.Error(err, "getting a writer from the connection")
}
errMsg := theErr.Error()
err = json.NewEncoder(writer).Encode(payload.Data{Error: errMsg})
if err != nil {
logger.Error(err, "error encoding the response to json")
}
err = writer.Close()
if err != nil {
logger.Error(err, "closing the writer")
}
}
func logRequestHandler(logger logr.Logger, h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logger.Info("Incoming request", "method", r.Method, "uri", r.URL.String(),
"referer", r.Header.Get("Referer"), "userAgent", r.Header.Get("User-Agent"))
h.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,153 @@
// [✓] Setup a cluster
// [✓] install crds on it
// - run the server locally
// - make requests to the server to see if we can get passphrases back
package challenger
import (
keyserverv1alpha1 "github.com/kairos-io/kairos-challenger/api/v1alpha1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("challenger", func() {
Describe("findSecretFor", func() {
var requestData PassphraseRequestData
var volumeList *keyserverv1alpha1.SealedVolumeList
BeforeEach(func() {
requestData = PassphraseRequestData{
TPMHash: "1234",
DeviceName: "/dev/sda1",
UUID: "sda1_uuid",
Label: "COS_PERSISTENT",
}
})
When("a sealedvolume matching the label exists", func() {
BeforeEach(func() {
volumeList = volumeListWithPartitionSpec(
keyserverv1alpha1.PartitionSpec{
Label: requestData.Label,
DeviceName: "not_matching",
UUID: "not_matching",
Secret: &keyserverv1alpha1.SecretSpec{
Name: "the_secret",
Path: "the_path",
}})
})
It("returns the sealed volume data", func() {
volumeData := findVolumeFor(requestData, volumeList)
Expect(volumeData).ToNot(BeNil())
Expect(volumeData.Quarantined).To(BeFalse())
Expect(volumeData.SecretName).To(Equal("the_secret"))
Expect(volumeData.SecretPath).To(Equal("the_path"))
})
})
When("a sealedvolume with empty field exists", func() {
BeforeEach(func() {
volumeList = volumeListWithPartitionSpec(
keyserverv1alpha1.PartitionSpec{
Label: "",
DeviceName: "not_matching",
UUID: "not_matching",
Secret: &keyserverv1alpha1.SecretSpec{
Name: "the_secret",
Path: "the_path",
}})
requestData = PassphraseRequestData{
TPMHash: "1234",
Label: "",
DeviceName: "/dev/sda1",
UUID: "sda1_uuid",
}
})
It("doesn't match a request with an empty field", func() {
volumeData := findVolumeFor(requestData, volumeList)
Expect(volumeData).To(BeNil())
})
})
When("a sealedvolume matching the device name exists", func() {
BeforeEach(func() {
volumeList = volumeListWithPartitionSpec(
keyserverv1alpha1.PartitionSpec{
Label: "not_matching",
DeviceName: requestData.DeviceName,
UUID: "not_matching",
Secret: &keyserverv1alpha1.SecretSpec{
Name: "the_secret",
Path: "the_path",
}})
})
It("returns the sealed volume data", func() {
volumeData := findVolumeFor(requestData, volumeList)
Expect(volumeData).ToNot(BeNil())
Expect(volumeData.Quarantined).To(BeFalse())
Expect(volumeData.SecretName).To(Equal("the_secret"))
Expect(volumeData.SecretPath).To(Equal("the_path"))
})
})
When("a sealedvolume matching the UUID exists", func() {
BeforeEach(func() {
volumeList = volumeListWithPartitionSpec(
keyserverv1alpha1.PartitionSpec{
Label: "not_matching",
DeviceName: "not_matching",
UUID: requestData.UUID,
Secret: &keyserverv1alpha1.SecretSpec{
Name: "the_secret",
Path: "the_path",
}})
})
It("returns the sealed volume data", func() {
volumeData := findVolumeFor(requestData, volumeList)
Expect(volumeData).ToNot(BeNil())
Expect(volumeData.Quarantined).To(BeFalse())
Expect(volumeData.SecretName).To(Equal("the_secret"))
Expect(volumeData.SecretPath).To(Equal("the_path"))
})
})
When("a matching sealedvolume doesn't exist", func() {
BeforeEach(func() {
volumeList = volumeListWithPartitionSpec(
keyserverv1alpha1.PartitionSpec{
Label: "not_matching",
DeviceName: "not_matching",
UUID: "not_matching",
Secret: &keyserverv1alpha1.SecretSpec{
Name: "the_secret",
Path: "the_path",
}})
})
It("returns nil sealedVolumeData", func() {
volumeData := findVolumeFor(requestData, volumeList)
Expect(volumeData).To(BeNil())
})
})
})
})
func volumeListWithPartitionSpec(partitionSpec keyserverv1alpha1.PartitionSpec) *keyserverv1alpha1.SealedVolumeList {
return &keyserverv1alpha1.SealedVolumeList{
Items: []keyserverv1alpha1.SealedVolume{
{Spec: keyserverv1alpha1.SealedVolumeSpec{
TPMHash: "1234",
Partitions: []keyserverv1alpha1.PartitionSpec{
partitionSpec,
},
Quarantined: false,
},
},
},
}
}

View File

@@ -0,0 +1,13 @@
package challenger_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestEpinio(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Kcrypt challenger suite")
}

4
pkg/constants/secret.go Normal file
View File

@@ -0,0 +1,4 @@
package constants
const TPMSecret = "tpm"
const GeneratedByKey = "generated_by"

19
pkg/payload/payload.go Normal file
View File

@@ -0,0 +1,19 @@
package payload
type Data struct {
Passphrase string `json:"passphrase"`
Error string `json:"error"`
GeneratedBy string `json:"generated_by"`
}
func (d Data) HasError() bool {
return d.Error != ""
}
func (d Data) HasPassphrase() bool {
return d.Passphrase != ""
}
func (d Data) HasBeenGenerated() bool {
return d.GeneratedBy != ""
}

18
renovate.json Normal file
View File

@@ -0,0 +1,18 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
],
"schedule": [
"after 11pm every weekday",
"before 7am every weekday",
"every weekend"
],
"timezone": "Europe/Brussels",
"packageRules": [
{
"matchUpdateTypes": ["patch"],
"automerge": true
}
]
}

62
scripts/e2e-tests.sh Executable file
View File

@@ -0,0 +1,62 @@
#!/bin/bash
set -e
# This scripts prepares a cluster where we install the kcrypt CRDs.
# This is where sealed volumes are created.
GINKGO_NODES="${GINKGO_NODES:-1}"
K3S_IMAGE="rancher/k3s:v1.26.1-k3s1"
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
CLUSTER_NAME=$(echo $RANDOM | md5sum | head -c 10; echo;)
export KUBECONFIG=$(mktemp)
# https://unix.stackexchange.com/a/423052
getFreePort() {
echo $(comm -23 <(seq "30000" "30200" | sort) <(ss -Htan | awk '{print $4}' | cut -d':' -f2 | sort -u) | shuf | head -n "1")
}
cleanup() {
echo "Cleaning up $CLUSTER_NAME"
k3d cluster delete "$CLUSTER_NAME" || true
rm -rf "$KUBECONFIG"
}
trap cleanup EXIT
# Create a cluster and bind ports 80 and 443 on the host
# This will allow us to access challenger server on 10.0.2.2 which is the IP
# on which qemu "sees" the host.
# We change the CIDR because k3s creates iptables rules that block DNS traffic to this CIDR
# (something like that). If you run k3d inside a k3s cluster (inside a Pod), DNS won't work
# inside the k3d server container unless you use a different CIDR.
# Here we are avoiding CIDR "10.43.x.x"
k3d cluster create "$CLUSTER_NAME" --k3s-arg "--cluster-cidr=10.49.0.1/16@server:0" --k3s-arg "--service-cidr=10.48.0.1/16@server:0" -p '80:80@server:0' -p '443:443@server:0' --image "$K3S_IMAGE"
k3d kubeconfig get "$CLUSTER_NAME" > "$KUBECONFIG"
# Build the docker image
IMG=controller:latest make docker-build
# Import the image to the cluster
k3d image import -c "$CLUSTER_NAME" controller:latest
# Install cert manager
kubectl apply -f https://github.com/jetstack/cert-manager/releases/latest/download/cert-manager.yaml
kubectl wait --for=condition=Available deployment --timeout=2m -n cert-manager --all
# Replace the CLUSTER_IP in the kustomize resource
# Only needed for debugging so that we can access the server from the host
# (the 10.0.2.2 IP address is only useful from within qemu)
export CLUSTER_IP=$(docker inspect "k3d-${CLUSTER_NAME}-server-0" | jq -r '.[0].NetworkSettings.Networks[].IPAddress')
envsubst \
< "$SCRIPT_DIR/../tests/assets/challenger-server-ingress.template.yaml" \
> "$SCRIPT_DIR/../tests/assets/challenger-server-ingress.yaml"
# Install the challenger server kustomization
kubectl apply -k "$SCRIPT_DIR/../tests/assets/"
# 10.0.2.2 is where the vm sees the host
# https://stackoverflow.com/a/6752280
export KMS_ADDRESS="10.0.2.2.challenger.sslip.io"
go run github.com/onsi/ginkgo/v2/ginkgo -v --nodes $GINKGO_NODES --label-filter $LABEL --fail-fast -r ./tests/

View File

@@ -0,0 +1,37 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
spec:
containers:
- name: manager
# Don't try to pull the image we built locally
imagePullPolicy: IfNotPresent
args:
- "--health-probe-bind-address"
- ":8081"
- "--metrics-bind-address"
- "127.0.0.1:8080"
- "--namespace"
- "default"
- "--leader-elect"
---
apiVersion: v1
kind: Service
metadata:
name: kcrypt-escrow-server
namespace: system
spec:
type: ClusterIP
selector:
control-plane: controller-manager
ports:
- name: wss
port: 8082
protocol: TCP
targetPort: 8082

View File

@@ -0,0 +1,35 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: challenger-server
annotations:
cert-manager.io/cluster-issuer: "selfsigned"
kubernetes.io/ingress.class: "traefik"
spec:
tls:
- hosts:
- 10.0.2.2.challenger.sslip.io
- ${CLUSTER_IP}.challenger.sslip.io
secretName: kms-tls
rules:
- host: 10.0.2.2.challenger.sslip.io
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: kcrypt-controller-kcrypt-escrow-server
port:
number: 8082
- host: ${CLUSTER_IP}.challenger.sslip.io
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: kcrypt-controller-kcrypt-escrow-server
port:
number: 8082

View File

@@ -0,0 +1,8 @@
---
# Self-signed issuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned
spec:
selfSigned: {}

View File

@@ -0,0 +1,13 @@
# Adds namespace to all resources.
namespace: default
bases:
- ../../config/default
resources:
- challenger-server-ingress.yaml
- cluster-issuer.yaml
patchesStrategicMerge:
# Fix labels and selectors to make challenger server accessible
- challenger-patch.yaml

391
tests/encryption_test.go Normal file
View File

@@ -0,0 +1,391 @@
package e2e_test
import (
"fmt"
"os"
"os/exec"
"path"
"strconv"
"strings"
"syscall"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/spectrocloud/peg/matcher"
"gopkg.in/yaml.v3"
client "github.com/kairos-io/kairos-challenger/cmd/discovery/client"
)
var installationOutput string
var vm VM
var _ = Describe("kcrypt encryption", func() {
var config string
BeforeEach(func() {
RegisterFailHandler(printInstallationOutput)
_, vm = startVM()
fmt.Printf("\nvm.StateDir = %+v\n", vm.StateDir)
vm.EventuallyConnects(1200)
})
JustBeforeEach(func() {
configFile, err := os.CreateTemp("", "")
Expect(err).ToNot(HaveOccurred())
defer os.Remove(configFile.Name())
err = os.WriteFile(configFile.Name(), []byte(config), 0744)
Expect(err).ToNot(HaveOccurred())
err = vm.Scp(configFile.Name(), "config.yaml", "0744")
Expect(err).ToNot(HaveOccurred())
installationOutput, err = vm.Sudo("/bin/bash -c 'set -o pipefail && kairos-agent manual-install --device auto config.yaml 2>&1 | tee manual-install.txt'")
Expect(err).ToNot(HaveOccurred(), installationOutput)
})
AfterEach(func() {
vm.GatherLog("/run/immucore/immucore.log")
err := vm.Destroy(func(vm VM) {
// Stop TPM emulator
tpmPID, err := os.ReadFile(path.Join(vm.StateDir, "tpm", "pid"))
Expect(err).ToNot(HaveOccurred())
if len(tpmPID) != 0 {
pid, err := strconv.Atoi(string(tpmPID))
Expect(err).ToNot(HaveOccurred())
syscall.Kill(pid, syscall.SIGKILL)
}
})
Expect(err).ToNot(HaveOccurred())
})
// https://kairos.io/docs/advanced/partition_encryption/#offline-mode
When("doing local encryption", Label("local-encryption"), func() {
BeforeEach(func() {
config = `#cloud-config
install:
encrypted_partitions:
- COS_PERSISTENT
reboot: false # we will reboot manually
hostname: metal-{{ trunc 4 .MachineID }}
users:
- name: kairos
passwd: kairos
`
})
It("boots and has an encrypted partition", func() {
vm.Reboot()
vm.EventuallyConnects(1200)
out, err := vm.Sudo("blkid")
Expect(err).ToNot(HaveOccurred(), out)
Expect(out).To(MatchRegexp("TYPE=\"crypto_LUKS\" PARTLABEL=\"persistent\""), out)
})
})
//https://kairos.io/docs/advanced/partition_encryption/#online-mode
When("using a remote key management server (automated passphrase generation)", Label("remote-auto"), func() {
var tpmHash string
var err error
BeforeEach(func() {
tpmHash, err = vm.Sudo("/system/discovery/kcrypt-discovery-challenger")
Expect(err).ToNot(HaveOccurred(), tpmHash)
kubectlApplyYaml(fmt.Sprintf(`---
apiVersion: keyserver.kairos.io/v1alpha1
kind: SealedVolume
metadata:
name: "%[1]s"
namespace: default
spec:
TPMHash: "%[1]s"
partitions:
- label: COS_PERSISTENT
quarantined: false
`, strings.TrimSpace(tpmHash)))
config = fmt.Sprintf(`#cloud-config
hostname: metal-{{ trunc 4 .MachineID }}
users:
- name: kairos
passwd: kairos
install:
encrypted_partitions:
- COS_PERSISTENT
grub_options:
extra_cmdline: "rd.neednet=1"
reboot: false # we will reboot manually
kcrypt:
challenger:
challenger_server: "http://%s"
nv_index: ""
c_index: ""
tpm_device: ""
`, os.Getenv("KMS_ADDRESS"))
})
AfterEach(func() {
cmd := exec.Command("kubectl", "delete", "sealedvolume", tpmHash)
out, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), out)
})
It("creates a passphrase and a key/pair to decrypt it", func() {
// Expect a LUKS partition
vm.Reboot(750)
vm.EventuallyConnects(1200)
out, err := vm.Sudo("blkid")
Expect(err).ToNot(HaveOccurred(), out)
Expect(out).To(MatchRegexp("TYPE=\"crypto_LUKS\" PARTLABEL=\"persistent\""), out)
// Expect a secret to be created
cmd := exec.Command("kubectl", "get", "secrets",
fmt.Sprintf("%s-cos-persistent", tpmHash),
"-o=go-template='{{.data.generated_by|base64decode}}'",
)
secretOut, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), string(secretOut))
Expect(string(secretOut)).To(MatchRegexp("tpm"))
})
})
// https://kairos.io/docs/advanced/partition_encryption/#scenario-static-keys
When("using a remote key management server (static keys)", Label("remote-static"), func() {
var tpmHash string
var err error
BeforeEach(func() {
tpmHash, err = vm.Sudo("/system/discovery/kcrypt-discovery-challenger")
Expect(err).ToNot(HaveOccurred(), tpmHash)
kubectlApplyYaml(fmt.Sprintf(`---
apiVersion: v1
kind: Secret
metadata:
name: %[1]s
namespace: default
type: Opaque
stringData:
pass: "awesome-plaintext-passphrase"
`, tpmHash))
kubectlApplyYaml(fmt.Sprintf(`---
apiVersion: keyserver.kairos.io/v1alpha1
kind: SealedVolume
metadata:
name: %[1]s
namespace: default
spec:
TPMHash: "%[1]s"
partitions:
- label: COS_PERSISTENT
secret:
name: %[1]s
path: pass
quarantined: false
`, tpmHash))
config = fmt.Sprintf(`#cloud-config
hostname: metal-{{ trunc 4 .MachineID }}
users:
- name: kairos
passwd: kairos
install:
encrypted_partitions:
- COS_PERSISTENT
grub_options:
extra_cmdline: "rd.neednet=1"
reboot: false # we will reboot manually
kcrypt:
challenger:
challenger_server: "http://%s"
nv_index: ""
c_index: ""
tpm_device: ""
`, os.Getenv("KMS_ADDRESS"))
})
AfterEach(func() {
cmd := exec.Command("kubectl", "delete", "sealedvolume", tpmHash)
out, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), out)
cmd = exec.Command("kubectl", "delete", "secret", tpmHash)
out, err = cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), out)
})
It("creates uses the existing passphrase to decrypt it", func() {
// Expect a LUKS partition
vm.Reboot()
vm.EventuallyConnects(1200)
out, err := vm.Sudo("blkid")
Expect(err).ToNot(HaveOccurred(), out)
Expect(out).To(MatchRegexp("TYPE=\"crypto_LUKS\" PARTLABEL=\"persistent\""), out)
Expect(out).To(MatchRegexp("/dev/mapper.*LABEL=\"COS_PERSISTENT\""), out)
})
})
When("the key management server is listening on https", func() {
var tpmHash string
var err error
BeforeEach(func() {
tpmHash, err = vm.Sudo("/system/discovery/kcrypt-discovery-challenger")
Expect(err).ToNot(HaveOccurred(), tpmHash)
kubectlApplyYaml(fmt.Sprintf(`---
apiVersion: keyserver.kairos.io/v1alpha1
kind: SealedVolume
metadata:
name: "%[1]s"
namespace: default
spec:
TPMHash: "%[1]s"
partitions:
- label: COS_PERSISTENT
quarantined: false
`, strings.TrimSpace(tpmHash)))
})
When("the certificate is pinned on the configuration", Label("remote-https-pinned"), func() {
BeforeEach(func() {
cert := getChallengerServerCert()
kcryptConfig := createConfigWithCert(fmt.Sprintf("https://%s", os.Getenv("KMS_ADDRESS")), cert)
kcryptConfigBytes, err := yaml.Marshal(kcryptConfig)
Expect(err).ToNot(HaveOccurred())
config = fmt.Sprintf(`#cloud-config
hostname: metal-{{ trunc 4 .MachineID }}
users:
- name: kairos
passwd: kairos
install:
encrypted_partitions:
- COS_PERSISTENT
grub_options:
extra_cmdline: "rd.neednet=1"
reboot: false # we will reboot manually
%s
`, string(kcryptConfigBytes))
})
It("successfully talks to the server", func() {
vm.Reboot()
vm.EventuallyConnects(1200)
out, err := vm.Sudo("blkid")
Expect(err).ToNot(HaveOccurred(), out)
Expect(out).To(MatchRegexp("TYPE=\"crypto_LUKS\" PARTLABEL=\"persistent\""), out)
Expect(out).To(MatchRegexp("/dev/mapper.*LABEL=\"COS_PERSISTENT\""), out)
})
})
When("the no certificate is set in the configuration", Label("remote-https-bad-cert"), func() {
BeforeEach(func() {
config = fmt.Sprintf(`#cloud-config
hostname: metal-{{ trunc 4 .MachineID }}
users:
- name: kairos
passwd: kairos
install:
encrypted_partitions:
- COS_PERSISTENT
grub_options:
extra_cmdline: "rd.neednet=1"
reboot: false # we will reboot manually
kcrypt:
challenger:
challenger_server: "https://%s"
nv_index: ""
c_index: ""
tpm_device: ""
`, os.Getenv("KMS_ADDRESS"))
})
It("fails to talk to the server", func() {
out, err := vm.Sudo("cat manual-install.txt")
Expect(err).ToNot(HaveOccurred(), out)
Expect(out).To(MatchRegexp("could not encrypt partition.*x509: certificate signed by unknown authority"))
})
})
})
})
func printInstallationOutput(message string, callerSkip ...int) {
fmt.Printf("This is the installation output in case it's useful:\n%s\n", installationOutput)
// Ensures the correct line numbers are reported
Fail(message, callerSkip[0]+1)
}
func kubectlApplyYaml(yamlData string) {
yamlFile, err := os.CreateTemp("", "")
Expect(err).ToNot(HaveOccurred())
defer os.Remove(yamlFile.Name())
err = os.WriteFile(yamlFile.Name(), []byte(yamlData), 0744)
Expect(err).ToNot(HaveOccurred())
cmd := exec.Command("kubectl", "apply", "-f", yamlFile.Name())
out, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), out)
}
func getChallengerServerCert() string {
cmd := exec.Command(
"kubectl", "get", "secret", "-n", "default", "kms-tls",
"-o", `go-template={{ index .data "ca.crt" | base64decode }}`)
out, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), string(out))
return string(out)
}
func createConfigWithCert(server, cert string) client.Config {
return client.Config{
Kcrypt: struct {
Challenger struct {
Server string "yaml:\"challenger_server,omitempty\""
NVIndex string "yaml:\"nv_index,omitempty\""
CIndex string "yaml:\"c_index,omitempty\""
TPMDevice string "yaml:\"tpm_device,omitempty\""
Certificate string "yaml:\"certificate,omitempty\""
}
}{
Challenger: struct {
Server string "yaml:\"challenger_server,omitempty\""
NVIndex string "yaml:\"nv_index,omitempty\""
CIndex string "yaml:\"c_index,omitempty\""
TPMDevice string "yaml:\"tpm_device,omitempty\""
Certificate string "yaml:\"certificate,omitempty\""
}{
Server: server,
NVIndex: "",
CIndex: "",
TPMDevice: "",
Certificate: cert,
},
},
}
}

191
tests/suite_test.go Normal file
View File

@@ -0,0 +1,191 @@
package e2e_test
import (
"context"
"fmt"
"net"
"os"
"os/exec"
"path"
"strconv"
"testing"
"github.com/google/uuid"
process "github.com/mudler/go-processmanager"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/spectrocloud/peg/matcher"
machine "github.com/spectrocloud/peg/pkg/machine"
"github.com/spectrocloud/peg/pkg/machine/types"
)
func TestE2e(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "kcrypt-challenger e2e test Suite")
}
func user() string {
user := os.Getenv("SSH_USER")
if user == "" {
user = "kairos"
}
return user
}
func pass() string {
pass := os.Getenv("SSH_PASS")
if pass == "" {
pass = "kairos"
}
return pass
}
func startVM() (context.Context, VM) {
if os.Getenv("ISO") == "" {
fmt.Println("ISO missing")
os.Exit(1)
}
vmName := uuid.New().String()
stateDir, err := os.MkdirTemp("", "")
Expect(err).ToNot(HaveOccurred())
emulateTPM(stateDir)
sshPort, err := getFreePort()
Expect(err).ToNot(HaveOccurred())
memory := os.Getenv("MEMORY")
if memory == "" {
memory = "2096"
}
cpus := os.Getenv("CPUS")
if cpus == "" {
cpus = "2"
}
opts := []types.MachineOption{
types.QEMUEngine,
types.WithISO(os.Getenv("ISO")),
types.WithMemory(memory),
types.WithCPU(cpus),
types.WithSSHPort(strconv.Itoa(sshPort)),
types.WithID(vmName),
types.WithSSHUser(user()),
types.WithSSHPass(pass()),
types.OnFailure(func(p *process.Process) {
defer GinkgoRecover()
var stdout, stderr, serial, status string
if stdoutBytes, err := os.ReadFile(p.StdoutPath()); err != nil {
stdout = fmt.Sprintf("Error reading stdout file: %s\n", err)
} else {
stdout = string(stdoutBytes)
}
if stderrBytes, err := os.ReadFile(p.StderrPath()); err != nil {
stderr = fmt.Sprintf("Error reading stderr file: %s\n", err)
} else {
stderr = string(stderrBytes)
}
if status, err = p.ExitCode(); err != nil {
status = fmt.Sprintf("Error reading exit code file: %s\n", err)
}
if serialBytes, err := os.ReadFile(path.Join(p.StateDir(), "serial.log")); err != nil {
serial = fmt.Sprintf("Error reading serial log file: %s\n", err)
} else {
serial = string(serialBytes)
}
Fail(fmt.Sprintf("\nVM Aborted.\nstdout: %s\nstderr: %s\nserial: %s\nExit status: %s\n",
stdout, stderr, serial, status))
}),
types.WithStateDir(stateDir),
// Serial output to file: https://superuser.com/a/1412150
func(m *types.MachineConfig) error {
m.Args = append(m.Args,
"-chardev", fmt.Sprintf("socket,id=chrtpm,path=%s/swtpm-sock", path.Join(stateDir, "tpm")),
"-tpmdev", "emulator,id=tpm0,chardev=chrtpm", "-device", "tpm-tis,tpmdev=tpm0",
"-chardev", fmt.Sprintf("stdio,mux=on,id=char0,logfile=%s,signal=off", path.Join(stateDir, "serial.log")),
"-serial", "chardev:char0",
"-mon", "chardev=char0",
)
return nil
},
}
// Set this to true to debug.
// You can connect to it with "spicy" or other tool.
var spicePort int
if os.Getenv("MACHINE_SPICY") != "" {
spicePort, err = getFreePort()
Expect(err).ToNot(HaveOccurred())
fmt.Printf("Spice port = %d\n", spicePort)
opts = append(opts, types.WithDisplay(fmt.Sprintf("-spice port=%d,addr=127.0.0.1,disable-ticketing", spicePort)))
}
if os.Getenv("KVM") != "" {
opts = append(opts, func(m *types.MachineConfig) error {
m.Args = append(m.Args,
"-enable-kvm",
)
return nil
})
}
m, err := machine.New(opts...)
Expect(err).ToNot(HaveOccurred())
vm := NewVM(m, stateDir)
ctx, err := vm.Start(context.Background())
Expect(err).ToNot(HaveOccurred())
if os.Getenv("MACHINE_SPICY") != "" {
cmd := exec.Command("spicy",
"-h", "127.0.0.1",
"-p", strconv.Itoa(spicePort))
err = cmd.Start()
Expect(err).ToNot(HaveOccurred())
}
return ctx, vm
}
// return the PID of the swtpm (to be killed later) and the state directory
func emulateTPM(stateDir string) {
t := path.Join(stateDir, "tpm")
err := os.MkdirAll(t, os.ModePerm)
Expect(err).ToNot(HaveOccurred())
cmd := exec.Command("swtpm",
"socket",
"--tpmstate", fmt.Sprintf("dir=%s", t),
"--ctrl", fmt.Sprintf("type=unixio,path=%s/swtpm-sock", t),
"--tpm2", "--log", "level=20")
err = cmd.Start()
Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(path.Join(t, "pid"), []byte(strconv.Itoa(cmd.Process.Pid)), 0744)
Expect(err).ToNot(HaveOccurred())
}
// https://gist.github.com/sevkin/96bdae9274465b2d09191384f86ef39d
// GetFreePort asks the kernel for a free open port that is ready to use.
func getFreePort() (port int, err error) {
var a *net.TCPAddr
if a, err = net.ResolveTCPAddr("tcp", "localhost:0"); err == nil {
var l *net.TCPListener
if l, err = net.ListenTCP("tcp", a); err == nil {
defer l.Close()
return l.Addr().(*net.TCPAddr).Port, nil
}
}
return
}