Forward ssh2 ports into docker
This commit is contained in:
parent
45f63935cd
commit
5bceaa0d47
24 changed files with 745 additions and 117 deletions
|
|
@ -1,104 +0,0 @@
|
|||
package ssh
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/sshx"
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
"github.com/tkw1536/proxyssh/feature"
|
||||
)
|
||||
|
||||
type SSH struct {
|
||||
component.Base
|
||||
Instances *instances.Instances
|
||||
}
|
||||
|
||||
const (
|
||||
etx rune = 3
|
||||
eot rune = 4
|
||||
)
|
||||
|
||||
const welcomeMessage = `Welcome to the WissKI SSH Server.
|
||||
You've successfully authenticated, but we don't provide shell access to the main server.
|
||||
You may use this connection as part of a proxy jump to connect to your server.
|
||||
For example:
|
||||
|
||||
ssh -J %s:2222 www-data@%s
|
||||
|
||||
Press CTRL-C to close this connection.
|
||||
`
|
||||
|
||||
// Server returns an ssh server that implements the main ssh server
|
||||
func (s *SSH) Server(context context.Context, ios stream.IOStream) (*ssh.Server, error) {
|
||||
var server ssh.Server
|
||||
|
||||
banner := fmt.Sprintf(welcomeMessage, s.Config.DefaultDomain, "example."+s.Config.DefaultDomain)
|
||||
|
||||
server.Handle(func(session ssh.Session) {
|
||||
io.WriteString(session, banner)
|
||||
|
||||
buffer := bufio.NewReader(session)
|
||||
for {
|
||||
res, _, err := buffer.ReadRune()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if res == etx || res == eot {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
server.PublicKeyHandler = feature.AuthorizeKeys(
|
||||
slogger{IOStream: ios},
|
||||
func(ctx ssh.Context) (keys []ssh.PublicKey, err error) {
|
||||
keys, err = s.GlobalKeys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instances, err := s.Instances.All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, instance := range instances {
|
||||
ikeys, err := instance.SSH().Keys()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, ikeys...)
|
||||
}
|
||||
return keys, nil
|
||||
})
|
||||
return &server, nil
|
||||
}
|
||||
|
||||
func (s *SSH) GlobalKeys() ([]ssh.PublicKey, error) {
|
||||
file, err := s.Environment.Open(s.Config.GlobalAuthorizedKeysFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sshx.ParseAllKeys(bytes), nil
|
||||
}
|
||||
|
||||
type slogger struct {
|
||||
stream.IOStream
|
||||
}
|
||||
|
||||
func (s slogger) Print(v ...any) {
|
||||
fmt.Fprint(s.Stderr, v...)
|
||||
}
|
||||
func (s slogger) Printf(format string, v ...any) {
|
||||
fmt.Fprintf(s.Stderr, format, v...)
|
||||
}
|
||||
38
internal/dis/component/ssh2/server.go
Normal file
38
internal/dis/component/ssh2/server.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package ssh2
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
)
|
||||
|
||||
const (
|
||||
etx rune = 3
|
||||
eot rune = 4
|
||||
)
|
||||
|
||||
const welcomeMessage = `Welcome to the WissKI SSH Server.
|
||||
You've successfully authenticated, but we don't provide shell access to the main server.
|
||||
You may use this connection as part of a proxy jump to connect to your server.
|
||||
For example:
|
||||
|
||||
ssh -J %s:2222 www-data@%s
|
||||
|
||||
Press CTRL-C to close this connection.
|
||||
`
|
||||
|
||||
// Server returns an ssh server that implements the main ssh server
|
||||
func (ssh2 *SSH2) Server(context context.Context, privateKeyPath string, io stream.IOStream) (*ssh.Server, error) {
|
||||
var server ssh.Server
|
||||
|
||||
if err := ssh2.setupHostKeys(io, privateKeyPath, &server); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ssh2.setupForwardHandler(&server)
|
||||
ssh2.setupHandler(&server)
|
||||
ssh2.setupAuth(&server)
|
||||
|
||||
return &server, nil
|
||||
}
|
||||
110
internal/dis/component/ssh2/server_auth.go
Normal file
110
internal/dis/component/ssh2/server_auth.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
package ssh2
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
)
|
||||
|
||||
func (ssh2 *SSH2) setupAuth(server *ssh.Server) {
|
||||
server.PublicKeyHandler = ssh2.handleAuth
|
||||
}
|
||||
|
||||
// ssh2Key is a type of context keys for this package
|
||||
type ssh2Key int
|
||||
|
||||
const (
|
||||
// permissions represents the permissions for the given session
|
||||
permission ssh2Key = iota
|
||||
)
|
||||
|
||||
func setPermissions(context ssh.Context, permissions map[string]bool) {
|
||||
context.SetValue(permission, permissions)
|
||||
}
|
||||
|
||||
// hasPermission checks if the given context permits access to the given slug.
|
||||
// The empty slug checks for global access.
|
||||
func hasPermission(context ssh.Context, slug string) bool {
|
||||
value, ok := context.Value(permission).(map[string]bool)
|
||||
return ok && value[slug]
|
||||
}
|
||||
|
||||
// getAnyPermission gets some instance the user has access to.
|
||||
// If the user does not have access to anything, returns "", false.
|
||||
// If the user has superuser access, but there are no instances, returns "", true.
|
||||
func getAnyPermission(context ssh.Context) (string, bool) {
|
||||
value, ok := context.Value(permission).(map[string]bool)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
for slug, ok := range value {
|
||||
if ok && slug != "" {
|
||||
return slug, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", (false || value[""])
|
||||
}
|
||||
|
||||
const authDelay = time.Second / 10
|
||||
|
||||
func (ssh2 *SSH2) handleAuth(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
return slowdown(func() (ok bool) {
|
||||
permissions := make(map[string]bool)
|
||||
|
||||
// grab the global permissions
|
||||
{
|
||||
globalKeys, err := ssh2.GlobalKeys()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
permissions[""] = isKey(globalKeys, key)
|
||||
ok = permissions[""]
|
||||
}
|
||||
|
||||
// grab permissions for each instance
|
||||
{
|
||||
instances, err := ssh2.Instances.All()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, instance := range instances {
|
||||
ikeys, err := instance.SSH().Keys()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
access := isKey(ikeys, key)
|
||||
|
||||
permissions[instance.Slug] = access || permissions[""]
|
||||
ok = ok || access
|
||||
}
|
||||
}
|
||||
|
||||
setPermissions(ctx, permissions)
|
||||
return
|
||||
}, authDelay)
|
||||
}
|
||||
|
||||
// slowdown invokes f immediatly, but only returns the result to the caller after at least duration.
|
||||
// It can be used to prevent timing attacks
|
||||
func slowdown[T any](f func() T, duration time.Duration) T {
|
||||
result := make(chan T, 1)
|
||||
go func() {
|
||||
result <- f()
|
||||
}()
|
||||
time.Sleep(duration)
|
||||
return <-result
|
||||
}
|
||||
|
||||
// isKey checks if keys contains key in O(len(keys))
|
||||
func isKey(keys []ssh.PublicKey, key ssh.PublicKey) bool {
|
||||
var res bool
|
||||
for _, ak := range keys {
|
||||
if ssh.KeysEqual(ak, key) {
|
||||
res = true
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
72
internal/dis/component/ssh2/server_forward.go
Normal file
72
internal/dis/component/ssh2/server_forward.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package ssh2
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// direct-tcpip data struct as specified in RFC4254, Section 7.2
|
||||
type localForwardChannelData struct {
|
||||
DestAddr string
|
||||
DestPort uint32
|
||||
|
||||
OriginAddr string
|
||||
OriginPort uint32
|
||||
}
|
||||
|
||||
// seetupForwardHandler sets up the forwarding handler for the ssh server
|
||||
func (ssh2 *SSH2) setupForwardHandler(server *ssh.Server) {
|
||||
if server.ChannelHandlers == nil {
|
||||
server.ChannelHandlers = make(map[string]ssh.ChannelHandler)
|
||||
for n, h := range ssh.DefaultChannelHandlers {
|
||||
server.ChannelHandlers[n] = h
|
||||
}
|
||||
}
|
||||
server.ChannelHandlers["direct-tcpip"] = ssh2.handleDirectTCP
|
||||
}
|
||||
|
||||
// handleDirectTCP handles a direct tcp connection for the server
|
||||
func (ssh2 *SSH2) handleDirectTCP(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
|
||||
d := localForwardChannelData{}
|
||||
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
|
||||
newChan.Reject(gossh.ConnectionFailed, "error parsing forward data: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
slug, ok := ssh2.Config.SlugFromHost(d.DestAddr)
|
||||
if !ok || d.DestPort != 22 || !hasPermission(ctx, slug) {
|
||||
newChan.Reject(gossh.Prohibited, "permission denied")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: move this into an instance function somewhere
|
||||
dest := net.JoinHostPort(slug+"."+ssh2.Config.DefaultDomain+".wisski", "22")
|
||||
|
||||
var dialer net.Dialer
|
||||
dconn, err := dialer.DialContext(ctx, "tcp", dest)
|
||||
if err != nil {
|
||||
newChan.Reject(gossh.ConnectionFailed, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ch, reqs, err := newChan.Accept()
|
||||
if err != nil {
|
||||
dconn.Close()
|
||||
return
|
||||
}
|
||||
go gossh.DiscardRequests(reqs)
|
||||
|
||||
go func() {
|
||||
defer ch.Close()
|
||||
defer dconn.Close()
|
||||
io.Copy(ch, dconn)
|
||||
}()
|
||||
go func() {
|
||||
defer ch.Close()
|
||||
defer dconn.Close()
|
||||
io.Copy(dconn, ch)
|
||||
}()
|
||||
}
|
||||
32
internal/dis/component/ssh2/server_handler.go
Normal file
32
internal/dis/component/ssh2/server_handler.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package ssh2
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
)
|
||||
|
||||
func (ssh2 *SSH2) setupHandler(server *ssh.Server) {
|
||||
server.Handle(ssh2.handleConnection)
|
||||
}
|
||||
|
||||
func (ssh2 *SSH2) handleConnection(session ssh.Session) {
|
||||
slug, _ := getAnyPermission(session.Context())
|
||||
banner := fmt.Sprintf(welcomeMessage, ssh2.Config.DefaultDomain, slug+"."+ssh2.Config.DefaultDomain)
|
||||
|
||||
io.WriteString(session, banner)
|
||||
|
||||
// wait until the user closes
|
||||
buffer := bufio.NewReader(session)
|
||||
for {
|
||||
res, _, err := buffer.ReadRune()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if res == etx || res == eot {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
304
internal/dis/component/ssh2/server_hostkeys.go
Normal file
304
internal/dis/component/ssh2/server_hostkeys.go
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
package ssh2
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
|
||||
"github.com/gliderlabs/ssh"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func (ssh2 *SSH2) setupHostKeys(io stream.IOStream, privateKeyPath string, server *ssh.Server) error {
|
||||
return ssh2.UseOrMakeHostKeys(io, server, privateKeyPath, nil)
|
||||
}
|
||||
|
||||
// UseOrMakeHostKeys is like UseOrMakeHostKey except that it accepts multiple HostKeyAlgorithms.
|
||||
// For each key algorithm, the privateKeyPath is appended with "_" + the name of the algorithm in question.
|
||||
//
|
||||
// When algorithms is nil, picks a reasonable set of default algorithms.
|
||||
func (ssh2 *SSH2) UseOrMakeHostKeys(io stream.IOStream, server *ssh.Server, privateKeyPath string, algorithms []HostKeyAlgorithm) error {
|
||||
if algorithms == nil {
|
||||
algorithms = []HostKeyAlgorithm{RSAAlgorithm, ED25519Algorithm}
|
||||
}
|
||||
|
||||
for _, algorithm := range algorithms {
|
||||
path := privateKeyPath + "_" + string(algorithm)
|
||||
if err := ssh2.UseOrMakeHostKey(io, server, path, algorithm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UseOrMakeHostKey attempts to load a host key from the given privateKeyPath.
|
||||
// If the path does not exist, a new host key is generated.
|
||||
// It then adds this hostkey to the priovided server.
|
||||
//
|
||||
// All parameters except the server are passed to ReadOrMakeHostKey.
|
||||
// Please see the appropriate documentation for that function.
|
||||
func (ssh2 *SSH2) UseOrMakeHostKey(io stream.IOStream, server *ssh.Server, privateKeyPath string, algorithm HostKeyAlgorithm) error {
|
||||
key, err := ssh2.ReadOrMakeHostKey(io, privateKeyPath, algorithm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// use the host key
|
||||
server.AddHostKey(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadOrMakeHostKey attempts to load a host key from the given privateKeyPath.
|
||||
// If the path does not exist, a new key is generated.
|
||||
//
|
||||
// This function assumes that if there is a host key in privateKeyPath it uses the provided HostKeyAlgorithm.
|
||||
// It makes no attempt at verifiying this; the key mail fail to load and return an error, or it may load incorrect data.
|
||||
func (ssh2 *SSH2) ReadOrMakeHostKey(io stream.IOStream, privateKeyPath string, algorithm HostKeyAlgorithm) (key gossh.Signer, err error) {
|
||||
hostKey := NewHostKey(algorithm)
|
||||
|
||||
if _, e := ssh2.Environment.Lstat(privateKeyPath); environment.IsNotExist(e) { // path doesn't exist => generate a new key there!
|
||||
err = ssh2.makeHostKey(io, hostKey, privateKeyPath)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "Unable to generate new host key")
|
||||
return
|
||||
}
|
||||
}
|
||||
err = ssh2.loadHostKey(io, hostKey, privateKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hostKey, nil
|
||||
}
|
||||
|
||||
// loadHostKey loadsa host key
|
||||
func (ssh2 *SSH2) loadHostKey(io stream.IOStream, key HostKey, path string) (err error) {
|
||||
io.EPrintf("Loading hostkey (algorithm %s) from %q", key.Algorithm(), path)
|
||||
|
||||
// read all the bytes from the file
|
||||
privateKeyBytes, err := environment.ReadFile(ssh2.Environment, path)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "Unable to read private key bytes")
|
||||
return
|
||||
}
|
||||
|
||||
// if the length is nil, return
|
||||
if len(privateKeyBytes) == 0 {
|
||||
err = errors.New("No bytes were read from the private key")
|
||||
return
|
||||
}
|
||||
|
||||
// decode the pem and unmarshal it
|
||||
privateKeyPEM, _ := pem.Decode(privateKeyBytes)
|
||||
if privateKeyPEM == nil {
|
||||
err = errors.New("pem.Decode() returned nil")
|
||||
return
|
||||
}
|
||||
return key.UnmarshalPEM(privateKeyPEM)
|
||||
}
|
||||
|
||||
// makeHostKey makes a new host key
|
||||
func (ssh2 *SSH2) makeHostKey(io stream.IOStream, key HostKey, path string) error {
|
||||
io.EPrintf("Writing hostkey (algorithm %s) to %q", key.Algorithm(), path)
|
||||
|
||||
if err := key.Generate(0, nil); err != nil {
|
||||
return errors.Wrap(err, "Failed to generate key")
|
||||
}
|
||||
|
||||
privateKeyPEM, err := key.MarshalPEM()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal key")
|
||||
}
|
||||
|
||||
// generate and write private key as PEM
|
||||
privateKeyFile, err := ssh2.Environment.Create(path, environment.DefaultFilePerm)
|
||||
defer privateKeyFile.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return pem.Encode(privateKeyFile, privateKeyPEM)
|
||||
}
|
||||
|
||||
// HostKey represents an pair of ssh private key and algorithm.
|
||||
// Once the hostkey is generated or loaded, it is safe for concurrent accesses.
|
||||
type HostKey interface {
|
||||
ssh.Signer
|
||||
|
||||
// Algorithm is the Algorithm used by this HostKey implementation.
|
||||
Algorithm() HostKeyAlgorithm
|
||||
|
||||
// Generate generates a new HostKey, discarding whatever was previsouly contained.
|
||||
//
|
||||
// keySize is the desired public key size in bits. When keySize is 0, a sensible default is used.
|
||||
// random is the source of randomness. If random is nil, crypto/rand.Reader will be used.
|
||||
Generate(keySize int, random io.Reader) error
|
||||
|
||||
// MarshalPEM marshals the private key into a pem.Block to be used for exporting.
|
||||
// The format is not guaranteed to follow any kind of standard, only that it is readable with the corresponding UnmarshalPEM.
|
||||
MarshalPEM() (*pem.Block, error)
|
||||
|
||||
// UnmarshalPEM unmarshals the private key from a pem.Block.
|
||||
// It is only compatible with whatever MarshalPEM() outputted.
|
||||
UnmarshalPEM(block *pem.Block) error
|
||||
}
|
||||
|
||||
// HostKeyAlgorithm is an enumerated value that represents a specific algorithm used for host keys.
|
||||
type HostKeyAlgorithm string
|
||||
|
||||
const (
|
||||
// RSAAlgorithm represents the RSA Algorithm
|
||||
RSAAlgorithm HostKeyAlgorithm = "rsa"
|
||||
|
||||
// ED25519Algorithm represents the ED25519 algorithm
|
||||
ED25519Algorithm HostKeyAlgorithm = "ed25519"
|
||||
)
|
||||
|
||||
// NewHostKey returns a new empty HostKey for the provided HostKey Algorithm.
|
||||
// An unsupported HostKeyAlgorithm will result in a call to panic().
|
||||
func NewHostKey(algorithm HostKeyAlgorithm) HostKey {
|
||||
switch algorithm {
|
||||
case RSAAlgorithm:
|
||||
return &rsaHostKey{defaultBitSize: 4096}
|
||||
case ED25519Algorithm:
|
||||
return &ed25519HostKey{}
|
||||
default:
|
||||
panic("Unsupported HostKeyAlgorithm")
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// ed25519 key
|
||||
//
|
||||
|
||||
type ed25519HostKey struct {
|
||||
ssh.Signer
|
||||
pk *ed25519.PrivateKey
|
||||
}
|
||||
|
||||
func init() {
|
||||
var _ HostKey = (*ed25519HostKey)(nil)
|
||||
}
|
||||
|
||||
func (ek *ed25519HostKey) Algorithm() HostKeyAlgorithm {
|
||||
return ED25519Algorithm
|
||||
}
|
||||
|
||||
var errKeySizeUnsupported = errors.New("ed25519HostKey.Generate(): keySize not supported")
|
||||
|
||||
func (ek *ed25519HostKey) Generate(keySize int, random io.Reader) (err error) {
|
||||
if keySize != 0 && keySize != ed25519.PublicKeySize {
|
||||
return errKeySizeUnsupported
|
||||
}
|
||||
if random == nil {
|
||||
random = rand.Reader
|
||||
}
|
||||
|
||||
_, pr, err := ed25519.GenerateKey(random)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// store the private key and setup the signer
|
||||
ek.pk = &pr
|
||||
ek.Signer, err = gossh.NewSignerFromKey(ek.pk)
|
||||
|
||||
// return
|
||||
return
|
||||
}
|
||||
|
||||
func (ek *ed25519HostKey) MarshalPEM() (block *pem.Block, err error) {
|
||||
block = &pem.Block{Type: "PRIVATE KEY", Bytes: ek.pk.Seed()}
|
||||
return
|
||||
}
|
||||
|
||||
func (ek *ed25519HostKey) UnmarshalPEM(block *pem.Block) (err error) {
|
||||
if block.Type != "PRIVATE KEY" {
|
||||
err = errors.New("Expected 'PRIVATE KEY' in PEM format")
|
||||
return
|
||||
}
|
||||
|
||||
pk := ed25519.NewKeyFromSeed(block.Bytes)
|
||||
|
||||
// store the private key and setup the signer
|
||||
ek.pk = &pk
|
||||
ek.Signer, err = gossh.NewSignerFromKey(ek.pk)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// rsa key
|
||||
//
|
||||
|
||||
type rsaHostKey struct {
|
||||
ssh.Signer
|
||||
|
||||
pk *rsa.PrivateKey
|
||||
|
||||
defaultBitSize int
|
||||
}
|
||||
|
||||
func init() {
|
||||
var _ HostKey = (*rsaHostKey)(nil)
|
||||
}
|
||||
|
||||
func (rk *rsaHostKey) Algorithm() HostKeyAlgorithm {
|
||||
return RSAAlgorithm
|
||||
}
|
||||
|
||||
func (rk *rsaHostKey) Generate(keySize int, random io.Reader) (err error) {
|
||||
if keySize == 0 {
|
||||
keySize = rk.defaultBitSize
|
||||
}
|
||||
if random == nil {
|
||||
random = rand.Reader
|
||||
}
|
||||
|
||||
rk.pk, err = rsa.GenerateKey(random, keySize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// store the signer
|
||||
rk.Signer, err = gossh.NewSignerFromKey(rk.pk)
|
||||
return
|
||||
}
|
||||
|
||||
func (rk *rsaHostKey) MarshalPEM() (block *pem.Block, err error) {
|
||||
block = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rk.pk)}
|
||||
return
|
||||
}
|
||||
|
||||
func (rk *rsaHostKey) UnmarshalPEM(block *pem.Block) (err error) {
|
||||
if block.Type != "RSA PRIVATE KEY" {
|
||||
err = errors.New("Expected 'RSA PRIVATE KEY' in PEM format")
|
||||
return
|
||||
}
|
||||
|
||||
// parse either a PKCS1 or PKCS8
|
||||
var parsedKey interface{}
|
||||
if parsedKey, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil {
|
||||
if parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil { // note this returns type `interface{}`
|
||||
err = errors.Wrap(err, "Expected PKCS1 or PKCS8 private key")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
pk, isRSA := parsedKey.(*rsa.PrivateKey)
|
||||
if !isRSA {
|
||||
err = errors.New("Expected an rsa.PrivateKey")
|
||||
return
|
||||
}
|
||||
|
||||
// store the private key and setup the signer
|
||||
rk.pk = pk
|
||||
rk.Signer, err = gossh.NewSignerFromKey(rk.pk)
|
||||
|
||||
return
|
||||
}
|
||||
10
internal/dis/component/ssh2/ssh2.env
Normal file
10
internal/dis/component/ssh2/ssh2.env
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
HOST_RULE=${HOST_RULE}
|
||||
|
||||
CONFIG_PATH=${CONFIG_PATH}
|
||||
DEPLOY_ROOT=${DEPLOY_ROOT}
|
||||
GLOBAL_AUTHORIZED_KEYS_FILE=${GLOBAL_AUTHORIZED_KEYS_FILE}
|
||||
SELF_OVERRIDES_FILE=${SELF_OVERRIDES_FILE}
|
||||
SELF_RESOLVER_BLOCK_FILE=${SELF_RESOLVER_BLOCK_FILE}
|
||||
|
||||
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}
|
||||
HTTPS_ENABLED=${HTTPS_ENABLED}
|
||||
28
internal/dis/component/ssh2/ssh2.go
Normal file
28
internal/dis/component/ssh2/ssh2.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package ssh2
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/sshx"
|
||||
"github.com/gliderlabs/ssh"
|
||||
)
|
||||
|
||||
type SSH2 struct {
|
||||
component.Base
|
||||
Instances *instances.Instances
|
||||
}
|
||||
|
||||
// GlobalKeys returns the global authorized keys
|
||||
func (s *SSH2) GlobalKeys() ([]ssh.PublicKey, error) {
|
||||
file, err := s.Environment.Open(s.Config.GlobalAuthorizedKeysFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sshx.ParseAllKeys(bytes), nil
|
||||
}
|
||||
1
internal/dis/component/ssh2/ssh2/.dockerignore
Normal file
1
internal/dis/component/ssh2/ssh2/.dockerignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
data
|
||||
5
internal/dis/component/ssh2/ssh2/Dockerfile
Normal file
5
internal/dis/component/ssh2/ssh2/Dockerfile
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
FROM docker.io/library/docker:20.10-cli
|
||||
|
||||
COPY wdcli /wdcli
|
||||
EXPOSE 2222
|
||||
CMD ["/wdcli","--internal-in-docker","ssh","--private-key-path", "/data/", "--bind","0.0.0.0:2222"]
|
||||
23
internal/dis/component/ssh2/ssh2/docker-compose.yml
Normal file
23
internal/dis/component/ssh2/ssh2/docker-compose.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
dis:
|
||||
build: .
|
||||
restart: always
|
||||
environment:
|
||||
CONFIG_PATH: ${CONFIG_PATH}
|
||||
ports:
|
||||
- "2223:2222"
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||
- "${CONFIG_PATH}:${CONFIG_PATH}:ro"
|
||||
- "${DEPLOY_ROOT}:${DEPLOY_ROOT}:rw"
|
||||
- "${GLOBAL_AUTHORIZED_KEYS_FILE}:${GLOBAL_AUTHORIZED_KEYS_FILE}:ro"
|
||||
- "${SELF_OVERRIDES_FILE}:${SELF_OVERRIDES_FILE}:ro"
|
||||
- "${SELF_RESOLVER_BLOCK_FILE}:${SELF_RESOLVER_BLOCK_FILE}:ro"
|
||||
- "./data/:/data/"
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: ${DOCKER_NETWORK_NAME}
|
||||
external: true
|
||||
47
internal/dis/component/ssh2/stack.go
Normal file
47
internal/dis/component/ssh2/stack.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package ssh2
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/bootstrap"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
|
||||
)
|
||||
|
||||
func (ssh SSH2) Path() string {
|
||||
return filepath.Join(ssh.Still.Config.DeployRoot, "core", "ssh2")
|
||||
}
|
||||
|
||||
//go:embed all:ssh2 ssh2.env
|
||||
var resources embed.FS
|
||||
|
||||
func (ssh *SSH2) Stack(env environment.Environment) component.StackWithResources {
|
||||
stt := component.MakeStack(ssh, env, component.StackWithResources{
|
||||
Resources: resources,
|
||||
ContextPath: "ssh2",
|
||||
EnvPath: "ssh2.env",
|
||||
|
||||
EnvContext: map[string]string{
|
||||
"DOCKER_NETWORK_NAME": ssh.Config.DockerNetworkName,
|
||||
"HOST_RULE": ssh.Config.DefaultHostRule(),
|
||||
"HTTPS_ENABLED": ssh.Config.HTTPSEnabledEnv(),
|
||||
|
||||
"CONFIG_PATH": ssh.Config.ConfigPath,
|
||||
"DEPLOY_ROOT": ssh.Config.DeployRoot,
|
||||
|
||||
"GLOBAL_AUTHORIZED_KEYS_FILE": ssh.Config.GlobalAuthorizedKeysFile,
|
||||
"SELF_OVERRIDES_FILE": ssh.Config.SelfOverridesFile,
|
||||
"SELF_RESOLVER_BLOCK_FILE": ssh.Config.SelfResolverBlockFile,
|
||||
},
|
||||
|
||||
CopyContextFiles: []string{bootstrap.Executable},
|
||||
})
|
||||
return stt
|
||||
}
|
||||
|
||||
func (ssh SSH2) Context(parent component.InstallationContext) component.InstallationContext {
|
||||
return component.InstallationContext{
|
||||
bootstrap.Executable: ssh.Config.CurrentExecutable(ssh.Environment), // TODO: Does this make sense?
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue