Skip to content

Commit

Permalink
Juju 7405/add fingerprint endpoint (#1530)
Browse files Browse the repository at this point in the history
* hook everything up and add fingerprint endpoint
  • Loading branch information
SimoneDutto authored Jan 23, 2025
1 parent c0b0b11 commit d97f72e
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/integration-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,8 @@ jobs:
juju deploy haproxy && \
sleep 5 && \
juju status
- name: Test ssh server is running
run: |
# TODO(simonedutto): improve this test when juju ssh controller is implemented.
nc -zv localhost 17022
38 changes: 30 additions & 8 deletions cmd/jimmsrv/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
jimmsvc "github.com/canonical/jimm/v3/cmd/jimmsrv/service"
"github.com/canonical/jimm/v3/internal/errors"
"github.com/canonical/jimm/v3/internal/logger"
"github.com/canonical/jimm/v3/internal/ssh"
"github.com/canonical/jimm/v3/version"
)

Expand Down Expand Up @@ -141,19 +142,29 @@ func start(ctx context.Context, s *service.Service) error {
return errors.E("jimm session store secret must be at least 64 characters")
}

hostKeyRaw := os.Getenv("JIMM_SSH_HOST_KEY")
if hostKeyRaw == "" {
return errors.E("empty hostkey from env variable")
}
fingerprints, err := ssh.GetFingerprintsFromPrivateKey([]byte(hostKeyRaw))
if err != nil {
return errors.E("cannot parse hostkey from env variable")
}

corsAllowedOrigins := strings.Split(os.Getenv("CORS_ALLOWED_ORIGINS"), " ")

logSQL, _ := strconv.ParseBool(os.Getenv("JIMM_LOG_SQL"))

jimmsvc, err := jimmsvc.NewService(ctx, jimmsvc.Params{
ControllerUUID: os.Getenv("JIMM_UUID"),
DSN: os.Getenv("JIMM_DSN"),
ControllerAdmins: strings.Fields(os.Getenv("JIMM_ADMINS")),
VaultRoleID: os.Getenv("VAULT_ROLE_ID"),
VaultRoleSecretID: os.Getenv("VAULT_ROLE_SECRET_ID"),
VaultAddress: os.Getenv("VAULT_ADDR"),
VaultPath: os.Getenv("VAULT_PATH"),
PublicDNSName: os.Getenv("JIMM_DNS_NAME"),
ControllerUUID: os.Getenv("JIMM_UUID"),
DSN: os.Getenv("JIMM_DSN"),
HostKeyFingerprints: fingerprints,
ControllerAdmins: strings.Fields(os.Getenv("JIMM_ADMINS")),
VaultRoleID: os.Getenv("VAULT_ROLE_ID"),
VaultRoleSecretID: os.Getenv("VAULT_ROLE_SECRET_ID"),
VaultAddress: os.Getenv("VAULT_ADDR"),
VaultPath: os.Getenv("VAULT_PATH"),
PublicDNSName: os.Getenv("JIMM_DNS_NAME"),
OpenFGAParams: jimmsvc.OpenFGAParams{
Scheme: os.Getenv("OPENFGA_SCHEME"),
Host: os.Getenv("OPENFGA_HOST"),
Expand Down Expand Up @@ -209,6 +220,17 @@ func start(ctx context.Context, s *service.Service) error {
})
s.Go(httpsrv.ListenAndServe)
zapctx.Info(ctx, "Successfully started JIMM server")
maxConccurentConncetions, _ := strconv.Atoi(os.Getenv("JIMM_SSH_MAX_CONCURRENT_CONNECTIONS"))

sshServer, err := ssh.NewJumpServer(ctx, ssh.Config{
Port: os.Getenv("JIMM_SSH_PORT"),
HostKey: []byte(hostKeyRaw),
MaxConcurrentConnections: maxConccurentConncetions,
}, jimmsvc.JIMM().SSHManager())
if err != nil {
return err
}
s.Go(sshServer.ListenAndServe)
zapctx.Info(ctx, "Successfully started JIMM ssh server")
return nil
}
6 changes: 6 additions & 0 deletions cmd/jimmsrv/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ type Params struct {
// LogLevel is the default logger is set.
// Setting this to "debug" enables the requests logger as well.
LogLevel string

// HostKeyFingerprints is the fingerprint of the SSH public host key.
HostKeyFingerprints map[string]string
}

// A Service is the implementation of a JIMM server.
Expand Down Expand Up @@ -496,6 +499,9 @@ func NewService(ctx context.Context, p Params) (*Service, error) {
jimmhttp.NewHTTPProxyHandler(s.jimm),
)

// serve the ssh public key fingerprint
s.mux.Get("/ssh/public-key-fingerprints", jimmhttp.WriteFingerprints(p.HostKeyFingerprints))

s.isLeader = p.IsLeader

return s, nil
Expand Down
40 changes: 40 additions & 0 deletions docker-compose.common.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,46 @@ services:
JIMM_SECURE_SESSION_COOKIES: false
JIMM_SESSION_COOKIE_MAX_AGE: 86400
JIMM_SESSION_SECRET_KEY: Xz2RkR9g87M75xfoumhEs5OmGziIX8D88Rk5YW8FSvkBPSgeK9t5AS9IvPDJ3NnB
JIMM_SSH_PORT: 17022
JIMM_SSH_HOST_KEY: |-
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----
healthcheck:
test: [ "CMD", "curl", "http://jimm.localhost:80" ]
interval: 5s
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ services:
container_name: jimm
ports:
- 17070:80
- 17022:17022
- 2345:2345
volumes:
- ./:/jimm/
Expand Down
27 changes: 27 additions & 0 deletions internal/jimm/jimm.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/canonical/jimm/v3/internal/jimm/permissions"
"github.com/canonical/jimm/v3/internal/jimm/role"
"github.com/canonical/jimm/v3/internal/jimm/serviceaccount"
"github.com/canonical/jimm/v3/internal/jimm/ssh"
"github.com/canonical/jimm/v3/internal/jimm/sshkeys"
"github.com/canonical/jimm/v3/internal/jimmjwx"
"github.com/canonical/jimm/v3/internal/openfga"
Expand Down Expand Up @@ -253,6 +254,18 @@ type SSHKeyManager interface {
RemoveUserKeyByComment(ctx context.Context, user *openfga.User, comment string) error
// RemoveUserKeyByFingerprint removes a user's public key(s) by the key fingerprint.
RemoveUserKeyByFingerprint(ctx context.Context, user *openfga.User, fingerprint string) error
// VerifyPublicKey lists the key for a user and compares the key to find a match.
VerifyPublicKey(ctx context.Context, claimUser string, publicKey []byte) (bool, error)
}

// SSHManager is the interface to enable the ssh server to operate. Performing public key verification and
// resolving addresses from model uuids.
type SSHManager interface {
// PublicKeyHandler is the method to verify the public key of the user. It returns a user if successful.
PublicKeyHandler(ctx context.Context, claimUser string, key []byte) (*openfga.User, error)

// ResolveAddressesFromModelUUID is the method to resolve the address of the controller to contact given the model UUID.
ResolveAddressesFromModelUUID(ctx context.Context, modelUUID string) ([]string, error)
}

// Parameters holds the services and static fields passed to the jimm.New() constructor.
Expand Down Expand Up @@ -402,6 +415,11 @@ func New(p Parameters) (*JIMM, error) {
}
j.sshKeyManager = sshKeyManager

sshManager, err := ssh.NewSSHManager(j.identityManager, j, j.sshKeyManager)
if err != nil {
return nil, err
}
j.sshManager = sshManager
return j, nil
}

Expand Down Expand Up @@ -436,6 +454,9 @@ type JIMM struct {

// sshKeyManager provides a means to manage SSH keys within JIMM.
sshKeyManager SSHKeyManager

// sshManager provides a means to manage SSH operations withing JIMM.
sshManager SSHManager
}

// ResourceTag returns JIMM's controller tag stating its UUID.
Expand Down Expand Up @@ -497,6 +518,12 @@ func (j *JIMM) SSHKeyManager() SSHKeyManager {
return j.sshKeyManager
}

// SSHManager returns a manager that enables operations
// related to ssh.
func (j *JIMM) SSHManager() SSHManager {
return j.sshManager
}

type permission struct {
resource string
relation string
Expand Down
29 changes: 29 additions & 0 deletions internal/jimmhttp/fingerprint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2025 Canonical.

package jimmhttp

import (
"encoding/json"
"net/http"

"github.com/juju/zaputil/zapctx"
"go.uber.org/zap"
)

// WriteFingerprints writes a map as JSON to the response.
func WriteFingerprints(m map[string]string) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
b, err := json.Marshal(m)
if err != nil {
zapctx.Error(ctx, "failed to marshal map", zap.Error(err))
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(b)
if err != nil {
zapctx.Error(ctx, "failed to write response", zap.Error(err))
}
}
}
2 changes: 1 addition & 1 deletion internal/jimmhttp/utils.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2024 Canonical.
// Copyright 2025 Canonical.

// Package jimmhttp contains utilities for HTTP connections.
package jimmhttp
Expand Down
1 change: 1 addition & 0 deletions internal/ssh/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func NewJumpServer(ctx context.Context, config Config, sshManager SSHManager) (S
return Server{}, fmt.Errorf("Cannot create JumpSSHServer with a nil ssh manager.")
}
config = setConfigDefaults(config)

server := Server{
Server: &ssh.Server{
Addr: fmt.Sprintf(":%s", config.Port),
Expand Down
20 changes: 20 additions & 0 deletions internal/ssh/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2025 Canonical.

package ssh

import (
gossh "golang.org/x/crypto/ssh"
)

// GetFingerprintsFromPrivateKey returns the fingerprints of the host key.
func GetFingerprintsFromPrivateKey(privateKey []byte) (map[string]string, error) {
key, err := gossh.ParsePrivateKey(privateKey)
if err != nil {
return nil, err
}

return map[string]string{
"SHA256": gossh.FingerprintSHA256(key.PublicKey()),
"MD5": gossh.FingerprintLegacyMD5(key.PublicKey()),
}, nil
}

0 comments on commit d97f72e

Please sign in to comment.