Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement RFC 16 to allow emergency node access #3557

Draft
wants to merge 45 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
a5124da
add `openssh-server` and `openssh` package
miampf Dec 3, 2024
340730d
`sshd` config and creation of `create-host-ssh-key` service
miampf Dec 5, 2024
60d703b
tf ssh access with custom lb
miampf Dec 12, 2024
188febd
`sshd` and `create-host-ssh-key` service on node
miampf Dec 6, 2024
2a5790f
terraform ssh setup
miampf Dec 17, 2024
e4fbbcb
change known_hosts file to writable location
miampf Dec 17, 2024
78d58ca
ssh node image configuration
miampf Dec 10, 2024
53b750a
nix fmt
miampf Dec 19, 2024
323bac7
add emergency_ssh var to other providers (untested)
miampf Jan 2, 2025
6eb3ed4
adjust `emergency_ssh` variable description
miampf Jan 2, 2025
28d1714
ProxyJump for hosts outside of 10.* range
miampf Jan 2, 2025
2511f4a
use `/run/ssh` subdir + harden openssh config a bit
miampf Jan 2, 2025
639b713
wrote docs for emergency ssh access workflow
miampf Jan 9, 2025
0092279
tidy check generate
miampf Jan 14, 2025
97dedf7
fix mirror from rebase
miampf Jan 21, 2025
efd0f48
Use `CertificateFile` instead of `IdentityFile`
miampf Jan 30, 2025
40744b6
Remove `AuthorizedKeysFile` setting
miampf Feb 5, 2025
68d0e50
update package hashes again
miampf Feb 5, 2025
4d80e75
Use correct pathing and improve CLI tip
miampf Feb 5, 2025
0f2d4bd
fix certificate formatting
miampf Feb 6, 2025
6f2f5b5
wrote ssh config specific info into docs
miampf Feb 11, 2025
0718f14
adjusted code accordingly
miampf Feb 11, 2025
eaaa8a5
Fix some vale errors
miampf Feb 11, 2025
c8472b1
bazel run //:generate
miampf Feb 11, 2025
8d76abc
Added `loadbalancer_address` output (important for e2e)
miampf Feb 13, 2025
bd3c259
Wrote structure for e2e test
miampf Feb 13, 2025
6031b22
Wrote `e2e_emergency_ssh` action
miampf Feb 13, 2025
90c4bc3
Transfer constellation workspace through actions
miampf Feb 13, 2025
c04c84e
Implemented `e2e-ssh` workflow
miampf Feb 13, 2025
ab47947
added emergency ssh test option
miampf Feb 13, 2025
f9df38c
added forgotten machine type
miampf Feb 13, 2025
950c854
ordered steps into step key
miampf Feb 13, 2025
ae197d5
fix indentation
miampf Feb 13, 2025
a64f8a8
don't error on "emergency ssh" input in e2e test action
miampf Feb 13, 2025
fb0563a
fix indentation again :)
miampf Feb 13, 2025
1926133
No custom working directory
miampf Feb 18, 2025
6298de9
use ssh command more appropriate for scripting
miampf Feb 18, 2025
d0b3bdb
loadbalancer address outputs for other GCP + AWS
miampf Feb 18, 2025
452b168
install terraform for e2e test
miampf Feb 18, 2025
bcaf376
correct location for `nixTools`
miampf Feb 18, 2025
b205b23
fix referenced variable names
miampf Feb 18, 2025
d88b1ae
do terraform stuff in terraform dir
miampf Feb 18, 2025
13fdf9b
[no ci] debug prints
miampf Feb 18, 2025
071d040
[no ci] fix typo in variable
miampf Feb 20, 2025
269e68f
dont check host keys
miampf Feb 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/actions/constellation_create/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ outputs:
osImageUsed:
description: "The OS image used in the cluster."
value: ${{ steps.setImage.outputs.image }}
workspace:
description: "The constellation workspace."
value: ${{ steps.setImage.outputs.workspace }}

runs:
using: "composite"
Expand Down Expand Up @@ -108,6 +111,7 @@ runs:

yq eval -i "(.image) = \"${imageInput}\"" constellation-conf.yaml
echo "image=${imageInput}" | tee -a "$GITHUB_OUTPUT"
echo "workspace=$(pwd)" | tee -a "$GITHUB_OUTPUT"

- name: Set marketplace image flag (AWS)
if: inputs.marketplaceImageVersion != '' && inputs.cloudProvider == 'aws'
Expand Down
51 changes: 51 additions & 0 deletions .github/actions/e2e_emergency_ssh/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Emergency ssh
description: "Verify that an emergency ssh connection can be established."

inputs:
workspace:
description: "The constellation workspace directory."
required: true
kubeconfig:
description: "The kubeconfig file for the cluster."
required: true

runs:
using: "composite"
steps:
- name: Test emergency ssh
shell: bash
env:
KUBECONFIG: ${{ inputs.kubeconfig }}
run: |
# Activate emergency ssh access to the cluster
pushd ./constellation-terraform
echo "emergency_ssh = true" >> terraform.tfvars
terraform apply -auto-approve
lb="$(terraform output -raw loadbalancer_address)"
popd

# write ssh config
cat > ssh_config <<EOF
Host $lb
ProxyJump none

Host *
StrictHostKeyChecking no
UserKnownHostsFile=/dev/null
IdentityFile ./access-key
PreferredAuthentications publickey
CertificateFile=constellation_cert.pub
User root
ProxyJump $lb
EOF

cat ssh_config

# generate and try keypair
ssh-keygen -t ecdsa -q -N "" -f ./access-key
constellation ssh --debug --key ./access-key.pub
internalIPs="$(kubectl get nodes -o=jsonpath='{.items[*].status.addresses}' | jq -r '.[] | select(.type == "InternalIP") | .address')"
for ip in $internalIPs; do
echo "Trying connection to $ip over $lb"
ssh -F ssh_config -o BatchMode=yes $ip true
done
13 changes: 11 additions & 2 deletions .github/actions/e2e_test/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ inputs:
description: "Azure credentials authorized to create an IAM configuration."
required: true
test:
description: "The test to run. Can currently be one of [sonobuoy full, sonobuoy quick, sonobuoy conformance, autoscaling, lb, perf-bench, verify, recover, malicious join, nop, upgrade]."
description: "The test to run. Can currently be one of [sonobuoy full, sonobuoy quick, sonobuoy conformance, autoscaling, lb, perf-bench, verify, recover, malicious join, nop, upgrade, emergency ssh]."
required: true
sonobuoyTestSuiteCmd:
description: "The sonobuoy test suite to run."
Expand Down Expand Up @@ -115,7 +115,7 @@ runs:
using: "composite"
steps:
- name: Check input
if: (!contains(fromJson('["sonobuoy full", "sonobuoy quick", "sonobuoy conformance", "autoscaling", "perf-bench", "verify", "lb", "recover", "malicious join", "s3proxy", "nop", "upgrade"]'), inputs.test))
if: (!contains(fromJson('["sonobuoy full", "sonobuoy quick", "sonobuoy conformance", "autoscaling", "perf-bench", "verify", "lb", "recover", "malicious join", "s3proxy", "nop", "upgrade", "emergency ssh"]'), inputs.test))
shell: bash
run: |
echo "::error::Invalid input for test field: ${{ inputs.test }}"
Expand Down Expand Up @@ -149,6 +149,8 @@ runs:

- name: Setup bazel
uses: ./.github/actions/setup_bazel_nix
with:
nixTools: terraform

- name: Log in to the Container registry
uses: ./.github/actions/container_registry_login
Expand Down Expand Up @@ -444,3 +446,10 @@ runs:
s3AccessKey: ${{ inputs.s3AccessKey }}
s3SecretKey: ${{ inputs.s3SecretKey }}
githubToken: ${{ inputs.githubToken }}

- name: Run emergency ssh test
if: inputs.test == 'emergency ssh'
uses: ./.github/actions/e2e_emergency_ssh
with:
kubeconfig: ${{ steps.constellation-create.outputs.kubeconfig }}
workspace: ${{ steps.constellation-create.outputs.workspace }}
99 changes: 99 additions & 0 deletions .github/workflows/e2e-ssh.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: e2e test emergency ssh

on:
workflow_dispatch:
push:
paths:
- cli/internal/cmd/ssh*.go
- terraform/infrastructure/aws/**
- terraform/infrastructure/azure/**
- terraform/infrastructure/gcp/**

jobs:
ssh:
runs-on: ubuntu-24.04
strategy:
matrix:
attestationVariant: ["gcp-sev-es", "gcp-sev-snp", "azure-sev-snp", "azure-tdx", "aws-sev-snp"]
steps:
- name: Checkout
id: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Get Latest Image
id: find-latest-image
uses: ./.github/actions/find_latest_image

- name: Split attestationVariant
id: split-attestationVariant
shell: bash
run: |
attestationVariant="${{ matrix.attestationVariant }}"
cloudProvider="${attestationVariant%%-*}"

echo "cloudProvider=${cloudProvider}" | tee -a "$GITHUB_OUTPUT"

- name: test
id: e2e_test
uses: ./.github/actions/e2e_test
with:
workerNodesCount: "1"
controlNodesCount: "1"
cloudProvider: ${{ steps.split-attestationVariant.outputs.cloudProvider }}
attestationVariant: ${{ matrix.attestationVariant }}
osImage: ${{ steps.find-latest-image.outputs.image }}
isDebugImage: ${{ steps.find-latest-image.outputs.isDebugImage }}
gcpProject: constellation-e2e
gcpClusterCreateServiceAccount: "[email protected]"
gcpIAMCreateServiceAccount: "[email protected]"
kubernetesVersion: "v1.28"
test: "emergency ssh"
azureSubscriptionID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
azureClusterCreateCredentials: ${{ secrets.AZURE_E2E_CLUSTER_CREDENTIALS }}
azureIAMCreateCredentials: ${{ secrets.AZURE_E2E_IAM_CREDENTIALS }}
registry: ghcr.io
githubToken: ${{ secrets.GITHUB_TOKEN }}
encryptionSecret: ${{ secrets.ARTIFACT_ENCRYPT_PASSWD }}

- name: Always terminate cluster
if: always()
uses: ./.github/actions/constellation_destroy
with:
kubeconfig: ${{ steps.e2e_test.outputs.kubeconfig }}
clusterCreation: "cli"
cloudProvider: ${{ steps.split-attestationVariant.outputs.cloudProvider }}
azureClusterDeleteCredentials: ${{ secrets.AZURE_E2E_CLUSTER_CREDENTIALS }}
gcpClusterDeleteServiceAccount: "[email protected]"

- name: Always delete IAM configuration
if: always()
uses: ./.github/actions/constellation_iam_destroy
with:
cloudProvider: ${{ steps.split-attestationVariant.outputs.cloudProvider }}
azureCredentials: ${{ secrets.AZURE_E2E_IAM_CREDENTIALS }}
gcpServiceAccount: "[email protected]"

- name: Update tfstate
if: always()
env:
GH_TOKEN: ${{ github.token }}
uses: ./.github/actions/update_tfstate
with:
name: terraform-state-${{ matrix.attestationVariant }}
runID: ${{ github.run_id }}
encryptionSecret: ${{ secrets.ARTIFACT_ENCRYPT_PASSWD }}

- name: Notify about failure
if: |
failure() &&
github.ref == 'refs/heads/main' &&
github.event_name == 'schedule'
continue-on-error: true
uses: ./.github/actions/notify_e2e_failure
with:
projectWriteToken: ${{ secrets.PROJECT_WRITE_TOKEN }}
test: "emergency ssh"
kubernetesVersion: "v1.28"
provider: ${{ steps.split-attestationVariant.outputs.cloudProvider }}
attestationVariant: ${{ matrix.attestationVariant }}
clusterCreation: "cli"
1 change: 1 addition & 0 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ on:
- "recover"
- "malicious join"
- "s3proxy"
- "emergency ssh"
- "nop"
required: true
kubernetesVersion:
Expand Down
25 changes: 8 additions & 17 deletions cli/internal/cmd/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"crypto/ed25519"
"crypto/rand"
"fmt"
"os"
"time"

"github.com/edgelesssys/constellation/v2/internal/constants"
Expand All @@ -28,8 +27,8 @@ import (
func NewSSHCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "ssh",
Short: "Prepare your cluster for emergency ssh access",
Long: "Prepare your cluster for emergency ssh access and sign a given key pair for authorization.",
Short: "Generate a certificate for emergency ssh access",
Long: "Generate a certificate for emergency ssh access to your ssh enabled constellation cluster.",
Args: cobra.ExactArgs(0),
RunE: runSSH,
}
Expand All @@ -54,18 +53,10 @@ func runSSH(cmd *cobra.Command, _ []string) error {
}

func writeCertificateForKey(cmd *cobra.Command, keyPath string, fh file.Handler, debugLogger debugLog) error {
_, err := fh.Stat(constants.TerraformWorkingDir)
if os.IsNotExist(err) {
return fmt.Errorf("directory %q does not exist", constants.TerraformWorkingDir)
}
if err != nil {
return err
}

// NOTE(miampf): Since other KMS aren't fully implemented yet, this commands assumes that the cKMS is used and derives the key accordingly.
var mastersecret uri.MasterSecret
if err = fh.ReadJSON(constants.MasterSecretFilename, &mastersecret); err != nil {
return fmt.Errorf("reading master secret: %s", err)
if err := fh.ReadJSON(constants.MasterSecretFilename, &mastersecret); err != nil {
return fmt.Errorf("reading master secret (does %q exist?): %s", constants.MasterSecretFilename, err)
}

mastersecretURI := uri.MasterSecret{Key: mastersecret.Key, Salt: mastersecret.Salt}
Expand Down Expand Up @@ -103,8 +94,8 @@ func writeCertificateForKey(cmd *cobra.Command, keyPath string, fh file.Handler,
ValidPrincipals: []string{"root"},
Permissions: ssh.Permissions{
Extensions: map[string]string{
"permit-port-forwarding": "yes",
"permit-pty": "yes",
"permit-port-forwarding": "",
"permit-pty": "",
},
},
}
Expand All @@ -113,10 +104,10 @@ func writeCertificateForKey(cmd *cobra.Command, keyPath string, fh file.Handler,
}

debugLogger.Debug("Signed certificate", "certificate", string(ssh.MarshalAuthorizedKey(&certificate)))
if err := fh.Write(fmt.Sprintf("%s/ca_cert.pub", constants.TerraformWorkingDir), ssh.MarshalAuthorizedKey(&certificate), file.OptOverwrite); err != nil {
if err := fh.Write("constellation_cert.pub", ssh.MarshalAuthorizedKey(&certificate), file.OptOverwrite); err != nil {
return fmt.Errorf("writing certificate: %s", err)
}
cmd.Printf("You can now connect to a node using 'ssh -F %s/ssh_config -i <your private key> <node ip>'.\nYou can obtain the private node IP via the web UI of your CSP.\n", constants.TerraformWorkingDir)
cmd.Printf("You can now connect to a node using the \"constellation_cert.pub\" certificate.\nLook at the documentation for a how to guide:\n\n\thttps://https://docs.edgeless.systems/constellation/workflows/troubleshooting#emergency-ssh-access\n")

return nil
}
31 changes: 6 additions & 25 deletions cli/internal/cmd/ssh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ package cmd

import (
"bytes"
"fmt"
"testing"

"github.com/edgelesssys/constellation/v2/internal/constants"
Expand All @@ -29,57 +28,39 @@ func TestSSH(t *testing.T) {
"salt": "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAK"
}
`

newFsWithDirectory := func() file.Handler {
require := require.New(t)
fh := file.NewHandler(afero.NewMemMapFs())
require.NoError(fh.MkdirAll(constants.TerraformWorkingDir))
return fh
}
newFsNoDirectory := func() file.Handler {
fh := file.NewHandler(afero.NewMemMapFs())
return fh
}

testCases := map[string]struct {
fh file.Handler
pubKey string
masterSecret string
wantErr bool
}{
"everything exists": {
fh: newFsWithDirectory(),
fh: file.NewHandler(afero.NewMemMapFs()),
pubKey: someSSHPubKey,
masterSecret: someMasterSecret,
},
"no public key": {
fh: newFsWithDirectory(),
fh: file.NewHandler(afero.NewMemMapFs()),
masterSecret: someMasterSecret,
wantErr: true,
},
"no master secret": {
fh: newFsWithDirectory(),
fh: file.NewHandler(afero.NewMemMapFs()),
pubKey: someSSHPubKey,
wantErr: true,
},
"malformed public key": {
fh: newFsWithDirectory(),
fh: file.NewHandler(afero.NewMemMapFs()),
pubKey: "asdf",
masterSecret: someMasterSecret,
wantErr: true,
},
"malformed master secret": {
fh: newFsWithDirectory(),
fh: file.NewHandler(afero.NewMemMapFs()),
masterSecret: "asdf",
pubKey: someSSHPubKey,
wantErr: true,
},
"directory does not exist": {
fh: newFsNoDirectory(),
pubKey: someSSHPubKey,
masterSecret: someMasterSecret,
wantErr: true,
},
}

for name, tc := range testCases {
Expand All @@ -104,7 +85,7 @@ func TestSSH(t *testing.T) {
assert.Error(err)
} else {
assert.NoError(err)
cert, err := tc.fh.Read(fmt.Sprintf("%s/ca_cert.pub", constants.TerraformWorkingDir))
cert, err := tc.fh.Read("constellation_cert.pub")
require.NoError(err)
_, _, _, _, err = ssh.ParseAuthorizedKey(cert)
require.NoError(err)
Expand Down
6 changes: 3 additions & 3 deletions docs/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Commands:
* [apply](#constellation-iam-upgrade-apply): Apply an upgrade to an IAM profile
* [version](#constellation-version): Display version of this CLI
* [init](#constellation-init): Initialize the Constellation cluster
* [ssh](#constellation-ssh): Prepare your cluster for emergency ssh access
* [ssh](#constellation-ssh): Generate a certificate for emergency ssh access

## constellation config

Expand Down Expand Up @@ -845,11 +845,11 @@ constellation init [flags]

## constellation ssh

Prepare your cluster for emergency ssh access
Generate a certificate for emergency ssh access

### Synopsis

Prepare your cluster for emergency ssh access and sign a given key pair for authorization.
Generate a certificate for emergency ssh access to your ssh enabled constellation cluster.

```
constellation ssh [flags]
Expand Down
Loading
Loading