Skip to content

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

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

Merged
merged 57 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
1af5518
image: add `openssh-server` and `openssh` package
miampf Dec 3, 2024
3477a23
image: `sshd` config + `create-host-ssh-key` service
miampf Dec 5, 2024
1297392
terraform: setup ssh access for azure
miampf Dec 12, 2024
52a6e07
image: setup for ssh connections
miampf Dec 17, 2024
13225b5
chore: formatting
miampf Dec 19, 2024
8db68c8
terraform: add `emergency_ssh` variable to AWS,GCP,openstack
miampf Jan 2, 2025
1f32349
terraform: adjust `emergency_ssh` variable description
miampf Jan 2, 2025
9c81aaa
terraform: allow connection to ssh hosts outside of `10.*` range
miampf Jan 2, 2025
96a1ac7
image: use `/run/ssh` directory for ssh files
miampf Jan 2, 2025
5a717db
docs: emergency ssh troubleshooting section
miampf Jan 9, 2025
3455829
chore: tidy, check, generate
miampf Jan 14, 2025
3e3ab95
terraform: use certificate in config
miampf Jan 30, 2025
bc39585
image: remove `AuthorizedKeysFile` setting
miampf Feb 5, 2025
00411b2
chore: fix rebase
miampf Feb 5, 2025
534492f
fix: use correct path to cert in CLI
miampf Feb 5, 2025
70bee6c
fix: correct certificate formatting in CLI
miampf Feb 6, 2025
3755da4
docs: wrote ssh config info
miampf Feb 11, 2025
cdb9ed2
cli,terraform: adjust code according to docs
miampf Feb 11, 2025
8b920e2
fix: vale errors
miampf Feb 11, 2025
077e00f
chore: bazel run //:generate
miampf Feb 11, 2025
6ebcb8d
terraform: add `loadbalancer_address` output for azure
miampf Feb 13, 2025
abc8733
e2e: initialize structure
miampf Feb 13, 2025
b44b6ae
e2e: add emergency ssh action
miampf Feb 13, 2025
56b415f
e2e: propagate create workspace
miampf Feb 13, 2025
1d87616
e2e: add emergency ssh workflow
miampf Feb 13, 2025
25a8226
e2e: add emergency ssh test option to workflow
miampf Feb 13, 2025
a310785
e2e: added forgotten machine type for runner
miampf Feb 13, 2025
0af238b
e2e: actually treat steps as steps
miampf Feb 13, 2025
adc4859
e2e: add `emergency ssh to e2e test action
miampf Feb 13, 2025
dcbf824
e2e: fix some problems in action
miampf Feb 13, 2025
abda8f6
terraform: add `loadbalancer_address` output to GCP and AWS
miampf Feb 18, 2025
924df54
e2e: install terraform for test
miampf Feb 18, 2025
5a3d20f
e2e: fix variable names
miampf Feb 18, 2025
c012b4b
e2e: switch to terraform directory correctly
miampf Feb 18, 2025
1634744
e2e: don't check host keys
miampf Feb 18, 2025
74ff522
chore: fix rebase
miampf Feb 25, 2025
7f9c313
fix: typo in CLI
miampf Feb 25, 2025
f83f0ee
terraform: add `loadbalancer_address` output to STACKIT
miampf Feb 25, 2025
4a5882d
terraform: `loadbalancer_address` outputs in correct section
miampf Feb 25, 2025
1381e94
e2e: correct permissions
miampf Feb 25, 2025
aaaa4d5
e2e: bump k8s version in workflow
miampf Feb 27, 2025
fb7d945
chore: update branch
miampf Mar 4, 2025
d5def19
e2e: improve test ergonomics & reliability
miampf Mar 4, 2025
c72c0ef
chore: fix rebase
miampf Mar 6, 2025
4aa1982
e2e: finish test
miampf Mar 6, 2025
4f5a9a6
chore: remove unwanted changes
miampf Mar 11, 2025
e60c1b3
chore: repair image hashsums
miampf Mar 18, 2025
c7d1e96
chore: implement style suggestions
miampf Mar 18, 2025
949f7fd
e2e: remove push trigger, add to weekly instead
miampf Mar 18, 2025
3f7a2b7
e2e: revert unwanted formatting changes
miampf Mar 18, 2025
69accbd
cli: ssh -> SSH in strings
miampf Mar 18, 2025
56b572a
docs: spelling changes
miampf Mar 18, 2025
30d79f9
docs: fix autoformatting
miampf Mar 18, 2025
74ebd46
e2e: remove `e2e-ssh.yml`
miampf Mar 18, 2025
eeeac9a
chore: bazel run //:generate
miampf Mar 18, 2025
3e418b7
docs: implement suggestions
miampf Mar 20, 2025
c24d3b1
chore: fix hashsums from rebase
miampf Mar 25, 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
68 changes: 68 additions & 0 deletions .github/actions/e2e_emergency_ssh/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Emergency ssh
description: "Verify that an emergency ssh connection can be established."

inputs:
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: |
set -euo pipefail

# 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

for i in {1..26}; do
if [[ "$i" -eq 26 ]]; then
echo "Port 22 never became reachable"
exit 1
fi
echo "Waiting until port 22 is reachable: $i/25"
if nc -z -w 25 "$lb" 22; then
break
fi
done

# 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
for i in {1..26}; do
if [[ "$i" -eq 26 ]]; then
echo "Failed to connect to $ip over $lb"
exit 1
fi
echo "Trying connection to $ip over $lb: $i/25"
if ssh -F ssh_config -o BatchMode=yes $ip true; then
echo "Connected to $ip successfully"
break
fi
done
done
12 changes: 10 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,9 @@ 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 }}
30 changes: 28 additions & 2 deletions .github/workflows/e2e-test-weekly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
strategy:
fail-fast: false
matrix:
refStream: ["ref/main/stream/nightly/?","ref/main/stream/debug/?", "ref/release/stream/stable/?"]
refStream: ["ref/main/stream/nightly/?", "ref/main/stream/debug/?", "ref/release/stream/stable/?"]
name: Find latest image
runs-on: ubuntu-24.04
permissions:
Expand Down Expand Up @@ -51,6 +51,33 @@ jobs:
# Tests on main-debug refStream
#

# Emergency SSH test on latest k8s version
- test: "emergency ssh"
refStream: "ref/main/stream/debug/?"
attestationVariant: "gcp-sev-es"
kubernetes-version: "v1.30"
clusterCreation: "cli"
- test: "emergency ssh"
refStream: "ref/main/stream/debug/?"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.30"
clusterCreation: "cli"
- test: "emergency ssh"
refStream: "ref/main/stream/debug/?"
attestationVariant: "azure-sev-snp"
kubernetes-version: "v1.30"
clusterCreation: "cli"
- test: "emergency ssh"
refStream: "ref/main/stream/debug/?"
attestationVariant: "azure-tdx"
kubernetes-version: "v1.30"
clusterCreation: "cli"
- test: "emergency ssh"
refStream: "ref/main/stream/debug/?"
attestationVariant: "aws-sev-snp"
kubernetes-version: "v1.30"
clusterCreation: "cli"

# Sonobuoy full test on latest k8s version
- test: "sonobuoy full"
refStream: "ref/main/stream/debug/?"
Expand Down Expand Up @@ -138,7 +165,6 @@ jobs:
kubernetes-version: "v1.29"
clusterCreation: "cli"


# verify test on latest k8s version
- test: "verify"
refStream: "ref/main/stream/debug/?"
Expand Down
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
29 changes: 10 additions & 19 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,12 +27,12 @@ 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,
}
cmd.Flags().String("key", "", "the path to an existing ssh public key")
cmd.Flags().String("key", "", "the path to an existing SSH public key")
must(cmd.MarkFlagRequired("key"))
return cmd
}
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?): %w", constants.MasterSecretFilename, err)
}

mastersecretURI := uri.MasterSecret{Key: mastersecret.Key, Salt: mastersecret.Salt}
Expand All @@ -80,7 +71,7 @@ func writeCertificateForKey(cmd *cobra.Command, keyPath string, fh file.Handler,

ca, err := crypto.GenerateEmergencySSHCAKey(sshCAKeySeed)
if err != nil {
return fmt.Errorf("generating ssh emergency CA key: %s", err)
return fmt.Errorf("generating SSH emergency CA key: %s", err)
}

debugLogger.Debug("SSH CA KEY generated", "public-key", string(ssh.MarshalAuthorizedKey(ca.PublicKey())))
Expand All @@ -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://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
8 changes: 4 additions & 4 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 All @@ -859,7 +859,7 @@ constellation ssh [flags]

```
-h, --help help for ssh
--key string the path to an existing ssh public key
--key string the path to an existing SSH public key
```

### Options inherited from parent commands
Expand Down
Loading