ssh: Update help page and allow service forwarding

This commit is contained in:
Tom Wiesing 2023-04-12 13:42:27 +02:00
parent eacd59bb1b
commit 85c63f24a9
No known key found for this signature in database
9 changed files with 166 additions and 41 deletions

View file

@ -10,6 +10,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/policy" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/policy"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2/sshkeys" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2/sshkeys"
"github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
@ -25,6 +26,7 @@ type UserPanel struct {
Instances *instances.Instances Instances *instances.Instances
Next *next.Next Next *next.Next
Keys *sshkeys.SSHKeys Keys *sshkeys.SSHKeys
SSH2 *ssh2.SSH2
} }
} }

View file

@ -8,6 +8,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2"
"github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/gliderlabs/ssh" "github.com/gliderlabs/ssh"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -38,6 +39,9 @@ type SSHTemplateContext struct {
Slug string // slug of the wisski Slug string // slug of the wisski
Hostname string // hostname of an example wisski Hostname string // hostname of an example wisski
// Services are the special services reachable via ssh
Services []ssh2.Intercept
} }
func (panel *UserPanel) sshRoute(ctx context.Context) http.Handler { func (panel *UserPanel) sshRoute(ctx context.Context) http.Handler {
@ -75,6 +79,8 @@ func (panel *UserPanel) sshRoute(ctx context.Context) http.Handler {
return sc, err return sc, err
} }
sc.Services = panel.Dependencies.SSH2.Intercepts()
return sc, nil return sc, nil
}) })
} }

View file

@ -5,6 +5,7 @@
</div> </div>
<div class="pure-u-1"> <div class="pure-u-1">
<h2>My SSH Keys</h2>
<p> <p>
This table shows ssh keys currently associated with your account. This table shows ssh keys currently associated with your account.
To add a new key, use the <em>Add New Key</em> button above. To add a new key, use the <em>Add New Key</em> button above.
@ -57,8 +58,8 @@
</div> </div>
</div> </div>
<div class="pure-u-1-2">
<div class="pure-u-1"> <h2 id="configuring-ssh-access">Configuring SSH Access</h2>
<p> <p>
You can use these ssh keys to connect to the distillery via ssh. You can use these ssh keys to connect to the distillery via ssh.
You can only connect to instances for which you appear as an <em>Administrator</em> on your user page. You can only connect to instances for which you appear as an <em>Administrator</em> on your user page.
@ -95,4 +96,46 @@ Host {{ .Domain }}.proxy
ssh {{ .Hostname }} ssh {{ .Hostname }}
</code> </code>
</div> </div>
<div class="pure-u-1-2">
<h2 id="accessing-services">Accessing Services</h2>
<p>
You can access the services powering the distillery if you have ssh access.
To access a service, simply use ssh port forwarding, and then access them under <code>localhost</code>.
Port forwarding is accomplished by appending <code>-L localport:service:serverport</code> to your ssh command line.
</p>
<p>
For example to connect to the triplestore, you can use:
</p>
<code class="copy">
<pre>
ssh -p {{ .Port }} {{ .Domain }} -L 7200:triplestore:7200
</pre>
</code>
<p>
and then go to <a target="_blank" rel="noopener noreferrer" href="http://127.0.0.1:7200/">http://127.0.0.1:7200/</a>.
</p>
<p>
Note that you can add port forwards both when connecting to the top-level distillery ssh server as well as any instance - the syntax is identical.
You can also add multiple forwards at the same time, by adding multiple <code>-L</code> arguments.
</p>
<p>
The complete list of services you can access are:
<ul>
{{ range .Services }}
<li>
<b>{{ .Description }}</b>, use <code class="copy">-L {{.ExamplePort}}:{{ .Match.Host }}:{{.Match.Port}}</code> and access at <code>127.0.0.1:{{.ExamplePort}}</code>
</li>
{{ end }}
</ul>
</p>
<p>
Depending on the service you may need an additional password.
Distillery administrators can reveal these passwords in the admin interface.
Furthermore instance-specific passwords can typically be found in the <em>Drupal Configuration</em>, system-wide passwords in the <em>Distillery Configuration</em>.
</p>
</div> </div>

View file

@ -2,7 +2,9 @@
package component package component
import ( import (
"net"
"reflect" "reflect"
"strconv"
"strings" "strings"
"github.com/FAU-CDI/wisski-distillery/internal/config" "github.com/FAU-CDI/wisski-distillery/internal/config"
@ -63,5 +65,34 @@ func (cb Base) ID() string {
// Still represents the central part of a distillery. // Still represents the central part of a distillery.
// It is used inside the main distillery struct, as well as every component via [ComponentBase]. // It is used inside the main distillery struct, as well as every component via [ComponentBase].
type Still struct { type Still struct {
Config *config.Config // the configuration of the distillery Config *config.Config // the configuration of the distillery
Upstream Upstream
}
// Upstream contains the configuration for accessing remote configuration.
type Upstream struct {
SQL HostPort
Triplestore HostPort
Solr HostPort
}
func (us Upstream) SQLAddr() string {
return us.SQL.String()
}
func (us Upstream) TriplestoreAddr() string {
return us.Triplestore.String()
}
func (us Upstream) SolrAddr() string {
return us.Solr.String()
}
type HostPort struct {
Host string
Port uint32
}
func (hp HostPort) String() string {
return net.JoinHostPort(hp.Host, strconv.Itoa(int(hp.Port)))
} }

View file

@ -1,9 +1,11 @@
package ssh2 package ssh2
import ( import (
"fmt"
"io" "io"
"net" "net"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/gliderlabs/ssh" "github.com/gliderlabs/ssh"
gossh "golang.org/x/crypto/ssh" gossh "golang.org/x/crypto/ssh"
) )
@ -17,7 +19,7 @@ type localForwardChannelData struct {
OriginPort uint32 OriginPort uint32
} }
// seetupForwardHandler sets up the forwarding handler for the ssh server // setupForwardHandler sets up the forwarding handler for the ssh server
func (ssh2 *SSH2) setupForwardHandler(server *ssh.Server) { func (ssh2 *SSH2) setupForwardHandler(server *ssh.Server) {
if server.ChannelHandlers == nil { if server.ChannelHandlers == nil {
server.ChannelHandlers = make(map[string]ssh.ChannelHandler) server.ChannelHandlers = make(map[string]ssh.ChannelHandler)
@ -28,6 +30,62 @@ func (ssh2 *SSH2) setupForwardHandler(server *ssh.Server) {
server.ChannelHandlers["direct-tcpip"] = ssh2.handleDirectTCP server.ChannelHandlers["direct-tcpip"] = ssh2.handleDirectTCP
} }
type Intercept struct {
Description string
Match component.HostPort
Dest component.HostPort
}
// ExamplePort returns a local port that can be forwarded to without root rights
func (i Intercept) ExamplePort() uint32 {
if i.Match.Port < 100 {
return i.Match.Port * 101
}
if i.Match.Port < 1024 {
return i.Match.Port * 1001
}
return i.Match.Port
}
func (i Intercept) Intercept(req component.HostPort) (intercepted bool, ok bool, dest component.HostPort, rejectReason string) {
if req.Host != i.Match.Host {
return false, ok, dest, rejectReason
}
if req.Port != i.Match.Port {
return true, false, dest, fmt.Sprintf("%s listens on port %d", i.Description, i.Match.Port)
}
return true, true, i.Dest, ""
}
func (ssh2 *SSH2) Intercepts() []Intercept {
return ssh2.interceptsC.Get(func() []Intercept {
return []Intercept{
{Description: "Triplestore", Match: component.HostPort{Host: "triplestore", Port: 7200}, Dest: ssh2.Upstream.Triplestore},
{Description: "SQL", Match: component.HostPort{Host: "sql", Port: 3306}, Dest: ssh2.Upstream.SQL},
{Description: "PHPMyAdmin", Match: component.HostPort{Host: "phpmyadmin", Port: 80}, Dest: component.HostPort{Host: "phpmyadmin", Port: 80}},
}
})
}
func (ssh2 *SSH2) getForwardDest(req component.HostPort, ctx ssh.Context) (ok bool, dest component.HostPort, rejectReason string) {
// check all the intercepts first
for _, i := range ssh2.Intercepts() {
intercepted, ok, dest, rejectReason := i.Intercept(req)
if !intercepted {
continue
}
return ok, dest, rejectReason
}
// then check the instances
slug, ok := ssh2.Config.HTTP.SlugFromHost(req.Host)
if !ok || req.Port != 22 || !hasPermission(ctx, slug) {
return false, dest, "permission denied"
}
return true, component.HostPort{Host: slug + "." + ssh2.Config.HTTP.PrimaryDomain + ".wisski", Port: 22}, ""
}
// handleDirectTCP handles a direct tcp connection for the server // 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) { func (ssh2 *SSH2) handleDirectTCP(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
d := localForwardChannelData{} d := localForwardChannelData{}
@ -36,18 +94,14 @@ func (ssh2 *SSH2) handleDirectTCP(srv *ssh.Server, conn *gossh.ServerConn, newCh
return return
} }
slug, ok := ssh2.Config.HTTP.SlugFromHost(d.DestAddr) ok, dest, rejectReason := ssh2.getForwardDest(component.HostPort{Host: d.DestAddr, Port: d.DestPort}, ctx)
if !ok || d.DestPort != 22 || !hasPermission(ctx, slug) { if !ok {
newChan.Reject(gossh.Prohibited, "permission denied") newChan.Reject(gossh.Prohibited, rejectReason)
return return
} }
// TODO: move this into an instance function somewhere
// NOTE(twiesing): This should be moved
dest := net.JoinHostPort(slug+"."+ssh2.Config.HTTP.PrimaryDomain+".wisski", "22")
var dialer net.Dialer var dialer net.Dialer
dconn, err := dialer.DialContext(ctx, "tcp", dest) dconn, err := dialer.DialContext(ctx, "tcp", dest.String())
if err != nil { if err != nil {
newChan.Reject(gossh.ConnectionFailed, err.Error()) newChan.Reject(gossh.ConnectionFailed, err.Error())
return return

View file

@ -6,6 +6,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/sql" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/sql"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2/sshkeys" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2/sshkeys"
"github.com/tkw1536/pkglib/lazy"
) )
type SSH2 struct { type SSH2 struct {
@ -16,6 +17,8 @@ type SSH2 struct {
Auth *auth.Auth Auth *auth.Auth
Keys *sshkeys.SSHKeys Keys *sshkeys.SSHKeys
} }
interceptsC lazy.Lazy[[]Intercept]
} }
var ( var (

View file

@ -8,7 +8,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
) )
func (ssh SSH2) Path() string { func (ssh *SSH2) Path() string {
return filepath.Join(ssh.Still.Config.Paths.Root, "core", "ssh2") return filepath.Join(ssh.Still.Config.Paths.Root, "core", "ssh2")
} }
@ -36,7 +36,7 @@ func (ssh *SSH2) Stack() component.StackWithResources {
}) })
} }
func (ssh SSH2) Context(parent component.InstallationContext) component.InstallationContext { func (ssh *SSH2) Context(parent component.InstallationContext) component.InstallationContext {
return component.InstallationContext{ return component.InstallationContext{
bootstrap.Executable: ssh.Config.Paths.CurrentExecutable(), // TODO: Does this make sense? bootstrap.Executable: ssh.Config.Paths.CurrentExecutable(), // TODO: Does this make sense?
} }

View file

@ -50,25 +50,11 @@ type Distillery struct {
// Where interactive progress is displayed // Where interactive progress is displayed
Progress io.Writer Progress io.Writer
// Upstream holds information to connect to the various running
// distillery components.
//
// NOTE(twiesing): This is intended to eventually allow full remote management of the distillery.
// But for now this will just hold upstream configuration.
Upstream Upstream
// lifetime holds all components // lifetime holds all components
lifetime lifetime.Lifetime[component.Component, component.Still] lifetime lifetime.Lifetime[component.Component, component.Still]
lifetimeInit sync.Once lifetimeInit sync.Once
} }
// Upstream contains the configuration for accessing remote configuration.
type Upstream struct {
SQL string
Triplestore string
Solr string
}
// //
// PUBLIC COMPONENT GETTERS // PUBLIC COMPONENT GETTERS
// //
@ -136,19 +122,19 @@ func (dis *Distillery) allComponents() []initFunc {
auto[*web.Web], auto[*web.Web],
manual(func(ts *triplestore.Triplestore) { manual(func(ts *triplestore.Triplestore) {
ts.BaseURL = "http://" + dis.Upstream.Triplestore ts.BaseURL = "http://" + dis.Upstream.TriplestoreAddr()
ts.PollInterval = time.Second ts.PollInterval = time.Second
}), }),
manual(func(sql *sql.SQL) { manual(func(sql *sql.SQL) {
sql.ServerURL = dis.Upstream.SQL sql.ServerURL = dis.Upstream.SQLAddr()
sql.PollInterval = time.Second sql.PollInterval = time.Second
}), }),
auto[*sql.LockTable], auto[*sql.LockTable],
auto[*sql.InstanceTable], auto[*sql.InstanceTable],
manual(func(s *solr.Solr) { manual(func(s *solr.Solr) {
s.BaseURL = dis.Upstream.Solr s.BaseURL = dis.Upstream.SolrAddr()
s.PollInterval = time.Second s.PollInterval = time.Second
}), }),

View file

@ -5,6 +5,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/cli" "github.com/FAU-CDI/wisski-distillery/internal/cli"
"github.com/FAU-CDI/wisski-distillery/internal/config" "github.com/FAU-CDI/wisski-distillery/internal/config"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/tkw1536/goprogram/exit" "github.com/tkw1536/goprogram/exit"
) )
@ -20,22 +21,21 @@ var errOpenConfig = exit.Error{
// NewDistillery creates a new distillery from the provided flags // NewDistillery creates a new distillery from the provided flags
func NewDistillery(params cli.Params, flags cli.Flags, req cli.Requirements) (dis *Distillery, err error) { func NewDistillery(params cli.Params, flags cli.Flags, req cli.Requirements) (dis *Distillery, err error) {
dis = &Distillery{ dis = new(Distillery)
Upstream: Upstream{ dis.Still.Upstream = component.Upstream{
SQL: "127.0.0.1:3306", SQL: component.HostPort{Host: "127.0.0.1", Port: 3306},
Triplestore: "127.0.0.1:7200", Triplestore: component.HostPort{Host: "127.0.0.1", Port: 7200},
Solr: "127.0.0.1:8983", Solr: component.HostPort{Host: "127.0.0.1", Port: 8983},
},
} }
// we are within the docker // we are within the docker
// //
// so setup the ports to connect everything to peroperly. // so setup the ports to connect everything to properly.
// also override some of the parameters for the environment. // also override some of the parameters for the environment.
if flags.InternalInDocker { if flags.InternalInDocker {
dis.Upstream.SQL = "sql:3306" dis.Still.Upstream.SQL = component.HostPort{Host: "sql", Port: 3306}
dis.Upstream.Triplestore = "triplestore:7200" dis.Still.Upstream.Triplestore = component.HostPort{Host: "triplestore", Port: 7200}
dis.Upstream.Solr = "solr:8983" dis.Still.Upstream.Solr = component.HostPort{Host: "solr", Port: 8983}
params.ConfigPath = os.Getenv("CONFIG_PATH") params.ConfigPath = os.Getenv("CONFIG_PATH")
} }