Forward ssh2 ports into docker

This commit is contained in:
Tom Wiesing 2022-11-11 16:06:59 +01:00
parent 45f63935cd
commit 5bceaa0d47
No known key found for this signature in database
24 changed files with 745 additions and 117 deletions

View file

@ -11,6 +11,7 @@ var SSH wisski_distillery.Command = ssh{}
type ssh struct {
Bind string `short:"b" long:"bind" description:"address to listen on" default:"127.0.0.1:2223"`
PrivateKeyPath string `short:"p" long:"private-key-path" description:"Path to store private host keys in" required:"1"`
}
func (s ssh) Description() wisski_distillery.Description {
@ -30,7 +31,7 @@ var errSSHListen = exit.Error{
func (s ssh) Run(context wisski_distillery.Context) error {
dis := context.Environment
server, err := dis.SSH().Server(dis.Context(), context.IOStream)
server, err := dis.SSH().Server(dis.Context(), s.PrivateKeyPath, context.IOStream)
if err != nil {
return err
}

3
go.mod
View file

@ -12,7 +12,7 @@ require (
github.com/gorilla/websocket v1.5.0
github.com/pkg/errors v0.9.1
github.com/tkw1536/goprogram v0.1.1
github.com/tkw1536/proxyssh v0.2.2
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d
golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0
gorm.io/driver/mysql v1.3.6
@ -26,7 +26,6 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d // indirect
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect
)

2
go.sum
View file

@ -29,8 +29,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/tkw1536/goprogram v0.1.1 h1:gamK9OuRqoX2yQlA/nkgfVHHZWd/u2uUj6vJMYrYa70=
github.com/tkw1536/goprogram v0.1.1/go.mod h1:Jqs0sTMzhrAGCX3JQrlEwQ0WRWQACCvuQQkaBDp65pE=
github.com/tkw1536/proxyssh v0.2.2 h1:2NVlTsRFFVfGaQH6B0Ci8n0ura1Qk4uEcZM0cDXAIF4=
github.com/tkw1536/proxyssh v0.2.2/go.mod h1:N6fExNESwKnKV1d0JGT3fvke+Aym22VNCAPEGJTofPQ=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741 h1:fGZugkZk2UgYBxtpKmvub51Yno1LJDeEsRp2xGD+0gY=

View file

@ -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...)
}

View 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
}

View 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
}

View 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)
}()
}

View 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
}
}
}

View 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
}

View 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}

View 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
}

View file

@ -0,0 +1 @@
data

View 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"]

View 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

View 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?
}
}

View file

@ -20,7 +20,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/resolver"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/solr"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/sql"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/triplestore"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/web"
"github.com/FAU-CDI/wisski-distillery/pkg/lazy"
@ -73,8 +73,8 @@ func (dis *Distillery) Resolver() *resolver.Resolver {
func (dis *Distillery) SQL() *sql.SQL {
return export[*sql.SQL](dis)
}
func (dis *Distillery) SSH() *ssh.SSH {
return export[*ssh.SSH](dis)
func (dis *Distillery) SSH() *ssh2.SSH2 {
return export[*ssh2.SSH2](dis)
}
func (dis *Distillery) Triplestore() *triplestore.Triplestore {
@ -138,7 +138,7 @@ func (dis *Distillery) allComponents() []initFunc {
auto[*exporter.Pathbuilders],
// ssh server
auto[*ssh.SSH],
auto[*ssh2.SSH2],
// Control server
auto[*control.Control],

View file

@ -5,4 +5,5 @@
!conf/*
!scripts/*
!patch/*
!ssh/*
!wisskiutils/*

View file

@ -2,9 +2,10 @@ FROM docker.io/library/php:8.0-apache-bullseye
ARG COMPOSER_VERSION=2.3.8
WORKDIR /var/www
# install and enable the various required php extension
RUN apt-get update && apt-get install -y \
# install and enable the various required php extensions and dropbear ssh server
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
curl \
openssh-server \
default-mysql-client \
git \
imagemagick \
@ -89,6 +90,7 @@ RUN a2ensite wisski
VOLUME /var/www/.composer
VOLUME /var/www/data
# Add and configure the entrypoint
ADD scripts/entrypoint.sh /entrypoint.sh
@ -101,6 +103,12 @@ ADD wisskiutils/ /wisskiutils
# Add the user_shell.sh
ADD scripts/user_shell.sh /user_shell.sh
ADD ssh/ /ssh/
VOLUME /ssh/hostkeys/
RUN chmod 700 /ssh/keys.sh && \
chmod 700 /ssh/start.sh && \
chmod 777 /user_shell.sh && \
chsh www-data --shell /user_shell.sh
# expose port 8080
EXPOSE 8080

View file

@ -24,6 +24,7 @@ services:
- ${GLOBAL_AUTHORIZED_KEYS_FILE}:/var/www/.ssh/global_authorized_keys:ro
- ${DATA_PATH}/.composer:/var/www/.composer
- ${DATA_PATH}/data:/var/www/data
- ${DATA_PATH}/hostkeys:/ssh/hostkeys:rw
- ${DATA_PATH}/authorized_keys:/var/www/.ssh/authorized_keys
- ${RUNTIME_DIR}:/runtime:ro

View file

@ -7,5 +7,8 @@ chown www-data:www-data /var/www
chown www-data:www-data /var/www/.composer
chown www-data:www-data /var/www/data/
# start up dropbear
/ssh/start.sh &
# run the original entrypoint
docker-php-entrypoint "$@"

View file

@ -1,5 +1,12 @@
#!/bin/bash
set -e
# This script is used to start a user shell inside the docker container.
cd "/var/www/data/project"
sudo -u www-data "PATH=/var/www/data/project/vendor/bin:$PATH" /bin/bash "$@"
export "PATH=/var/www/data/project/vendor/bin:$PATH"
if [ "$USER" = "www-data" ]; then
/bin/bash "$@"
else
sudo -u www-data /bin/bash "$@"
fi;

View file

@ -0,0 +1,3 @@
#!/bin/bash
cat /var/www/.ssh/authorized_keys /var/www/.ssh/global_authorized_keys 2> /dev/null || exit 0

View file

@ -0,0 +1,27 @@
# sshd_config file for distillery ssh server
# listen on port 22
Port 22
ListenAddress 0.0.0.0
# Use hostkeys from /ssh/hostkeys
HostKey /ssh/hostkeys/ssh_host_rsa_key
HostKey /ssh/hostkeys/ssh_host_ecdsa_key
HostKey /ssh/hostkeys/ssh_host_ed25519_key
# Disable forwarding and motd
X11Forwarding no
PrintMotd no
# allow sftp
Subsystem sftp /usr/lib/openssh/sftp-server
# allow only www-data to login
AllowUsers www-data
# allow only public keys using /ssh/keys.sh
PubkeyAuthentication yes
AuthenticationMethods publickey
AuthorizedKeysFile none
AuthorizedKeysCommand /ssh/keys.sh
AuthorizedKeysCommandUser root

View file

@ -0,0 +1,14 @@
#!/bin/bash
# create the sshd directory
if [ ! -d /run/sshd ]; then
mkdir /run/sshd
chmod 0755 /run/sshd
fi
# regenerate key files if they do not yet exist
[[ -f "/ssh/hostkeys/ssh_host_rsa_key" ]] || ssh-keygen -q -N "" -t dsa -f /ssh/hostkeys/ssh_host_rsa_key
[[ -f "/ssh/hostkeys/ssh_host_ecdsa_key" ]] || ssh-keygen -q -N "" -t ecdsa -f /ssh/hostkeys/ssh_host_ecdsa_key
[[ -f "/ssh/hostkeys/ssh_host_ed25519_key" ]] || ssh-keygen -q -N "" -t ed25519 -f /ssh/hostkeys/ssh_host_ed25519_key
/usr/sbin/sshd -e -D -f /ssh/sshd_config