Compare commits
22 Commits
v1.19.0
...
001-proxy-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85cabeefa5 | ||
|
|
f9c8f60365 | ||
|
|
db5cfec59a | ||
|
|
672ef1724e | ||
|
|
35b76f9063 | ||
|
|
a30952348b | ||
|
|
dd4a21032f | ||
|
|
73926212d5 | ||
|
|
773b7d5a8b | ||
|
|
3ee75e47dd | ||
|
|
bb6e7c46cc | ||
|
|
111ced03ad | ||
|
|
46970b6d17 | ||
|
|
afc3888afe | ||
|
|
7ecdb808df | ||
|
|
d3d45da163 | ||
|
|
2287353585 | ||
|
|
ee5a89413e | ||
|
|
9b30972e1e | ||
|
|
9b849441fa | ||
|
|
0a6ee0f985 | ||
|
|
3610fbeb04 |
16
.gitea/workflows/ci.yaml
Normal file
16
.gitea/workflows/ci.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
docker-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build the Docker image
|
||||
run: docker build . --file Dockerfile
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -20,9 +20,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: lint
|
||||
uses: golangci/golangci-lint-action@v2.5.2
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: v1.38
|
||||
version: v1.50.1
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tests-on-windows:
|
||||
needs: golangci-lint # run after golangci-lint action to not produce duplicated errors
|
||||
|
||||
21
.github/workflows/semgrep.yml
vendored
Normal file
21
.github/workflows/semgrep.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
on:
|
||||
pull_request: {}
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- .github/workflows/semgrep.yml
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
name: Semgrep
|
||||
jobs:
|
||||
semgrep:
|
||||
name: Scan
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
|
||||
container:
|
||||
image: returntocorp/semgrep
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: semgrep ci
|
||||
@@ -47,7 +47,7 @@ linters:
|
||||
- staticcheck
|
||||
- structcheck
|
||||
#- stylecheck
|
||||
- typecheck
|
||||
#- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# build
|
||||
FROM golang:1.17.6 as builder
|
||||
FROM golang:1.18.0 as builder
|
||||
ENV GO111MODULE=on
|
||||
WORKDIR /go/src/moul.io/sshportal
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
14
README.md
14
README.md
@@ -61,7 +61,7 @@ Shared connection to localhost closed.
|
||||
$
|
||||
```
|
||||
|
||||
If the association fails and you are promted for a password, verify that the host you're connecting from has a SSH key set up or generate one with ```ssh-keygen -t rsa```
|
||||
If the association fails and you are prompted for a password, verify that the host you're connecting from has a SSH key set up or generate one with ```ssh-keygen -t rsa```
|
||||
|
||||
Drop an interactive administrator shell
|
||||
|
||||
@@ -135,7 +135,7 @@ Used by educators to provide temporary access to students. [Feedback from a teac
|
||||
|
||||
There are companies who use a jump host to monitor connections at a single point.
|
||||
|
||||
A hosting company is using SSHportal for its “logging” feature, among the others. As every session is logged and introspectable, they have a detailed history of who performed which action. This company made its own contribution on the project, allowing the support of [more than 65.000 sessions in the database](https://github.com/moul/sshportal/pull/76).
|
||||
A hosting company is using SSHportal for its “logging” feature, among others. As every session is logged and introspectable, they have a detailed history of who performed which action. This company made its own contribution to the project, allowing the support of [more than 65.000 sessions in the database](https://github.com/moul/sshportal/pull/76).
|
||||
|
||||
The project has also received [multiple contributions from a security researcher](https://github.com/moul/sshportal/pulls?q=is%3Apr+author%3Asabban+sort%3Aupdated-desc) that made a thesis on quantum cryptography. This person uses SSHportal in their security-hardened hosting company.
|
||||
|
||||
@@ -155,7 +155,7 @@ If you need to invite multiple people to an event (hackathon, course, etc), the
|
||||
* User management (invite, group, stats)
|
||||
* Host Key management (create, remove, update, import)
|
||||
* Automatic remote host key learning
|
||||
* User Key management (multile keys per user)
|
||||
* User Key management (multiple keys per user)
|
||||
* ACL management (acl+user-groups+host-groups)
|
||||
* User roles (admin, trusted, standard, ...)
|
||||
* User invitations (no more "give me your public ssh key please")
|
||||
@@ -184,7 +184,7 @@ If you need to invite multiple people to an event (hackathon, course, etc), the
|
||||
|
||||
* Does not work (yet?) with [`mosh`](https://mosh.org/)
|
||||
* It is not possible for a user to access a host with the same name as the user. This is easily circumvented by changing the user name, especially since the most common use cases does not expose it.
|
||||
* It is not possible access a host named `healthcheck` as this is a built in command.
|
||||
* It is not possible to access a host named `healthcheck` as this is a built-in command.
|
||||
|
||||
---
|
||||
|
||||
@@ -215,7 +215,7 @@ cp sshportal.db sshportal.db.bkp
|
||||
|
||||
# run the new version
|
||||
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.10.0
|
||||
# check the logs for migration or cross-version incompabitility errors
|
||||
# check the logs for migration or cross-version incompatibility errors
|
||||
docker logs -f sshportal
|
||||
```
|
||||
|
||||
@@ -276,7 +276,7 @@ cp sshportal.db sshportal.db.bkp
|
||||
|
||||
By default, the configuration user is `admin`, (can be changed using `--config-user=<value>` when starting the server. The shell is also accessible through `ssh [username]@portal.example.org`.
|
||||
|
||||
Each commands can be run directly by using this syntax: `ssh admin@portal.example.org <command> [args]`:
|
||||
Each command can be run directly by using this syntax: `ssh admin@portal.example.org <command> [args]`:
|
||||
|
||||
```
|
||||
ssh admin@portal.example.org host inspect toto
|
||||
@@ -446,7 +446,7 @@ ssh localhost -p 2222 -l hostname
|
||||
|
||||
By default, `sshportal` uses a local [sqlite](https://www.sqlite.org/) database which isn't scalable by design.
|
||||
|
||||
You can run multiple instances of `sshportal` sharing a same [MySQL](https://www.mysql.com) database, using `sshportal --db-conn=user:pass@host/dbname?parseTime=true --db-driver=mysql`.
|
||||
You can run multiple instances of `sshportal` sharing the same [MySQL](https://www.mysql.com) database, using `sshportal --db-conn=user:pass@host/dbname?parseTime=true --db-driver=mysql`.
|
||||
|
||||

|
||||
|
||||
|
||||
5
go.mod
generated
5
go.mod
generated
@@ -14,6 +14,7 @@ require (
|
||||
github.com/mattn/go-runewidth v0.0.12 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/pires/go-proxyproto v0.6.2 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/reiver/go-oi v1.0.0
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
|
||||
@@ -23,9 +24,9 @@ require (
|
||||
github.com/smartystreets/goconvey v1.7.2
|
||||
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
|
||||
github.com/urfave/cli v1.22.5
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
|
||||
golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab
|
||||
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed // indirect
|
||||
golang.org/x/tools v0.1.8
|
||||
golang.org/x/tools v0.1.10
|
||||
gorm.io/driver/mysql v1.2.3
|
||||
gorm.io/driver/postgres v1.2.3
|
||||
gorm.io/driver/sqlite v1.2.6
|
||||
|
||||
14
go.sum
generated
14
go.sum
generated
@@ -158,6 +158,8 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQ
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8=
|
||||
github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 h1:+/+DxvQaYifJ+grD4klzrS5y+KJXldn/2YTl5JG+vZ8=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -235,15 +237,15 @@ golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWP
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab h1:lnZ4LoV0UMdibeCUfIB2a4uFwRu491WX/VB2reB8xNc=
|
||||
golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -299,8 +301,8 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w=
|
||||
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
15
main.go
15
main.go
@@ -79,12 +79,17 @@ func main() {
|
||||
Value: 0,
|
||||
Usage: "Duration before an inactive connection is timed out (0 to disable)",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "acl-check-cmd",
|
||||
EnvVar: "SSHPORTAL_ACL_CHECK_CMD",
|
||||
Usage: "Execute external command to check ACL",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "acl-check-cmd",
|
||||
EnvVar: "SSHPORTAL_ACL_CHECK_CMD",
|
||||
Usage: "Execute external command to check ACL",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-proxy-protocol",
|
||||
EnvVar: "SSHPORTAL_NO_PROXY_PROTOCOL",
|
||||
Usage: "Disable Proxy Protocol auto-detection (default: enabled)",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
Name: "healthcheck",
|
||||
Action: func(c *cli.Context) error { return healthcheck(c.String("addr"), c.Bool("wait"), c.Bool("quiet")) },
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"math/big"
|
||||
"os"
|
||||
"os/user"
|
||||
"strings"
|
||||
@@ -617,7 +618,10 @@ func DBInit(db *gorm.DB) error {
|
||||
}
|
||||
if count == 0 {
|
||||
// if no admin, create an account for the first connection
|
||||
inviteToken := randStringBytes(16)
|
||||
inviteToken, err := randStringBytes(16)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN") != "" {
|
||||
inviteToken = os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN")
|
||||
}
|
||||
@@ -673,12 +677,16 @@ func DBInit(db *gorm.DB) error {
|
||||
}).Error
|
||||
}
|
||||
|
||||
func randStringBytes(n int) string {
|
||||
func randStringBytes(n int) (string, error) {
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||
r, err := rand.Int(rand.Reader, big.NewInt(int64(len(letterBytes))))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate random string: %s", err)
|
||||
}
|
||||
b[i] = letterBytes[r.Int64()]
|
||||
}
|
||||
return string(b)
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
@@ -5,21 +5,83 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/pires/go-proxyproto"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sabban/bastion/pkg/logchannel"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type sessionConfig struct {
|
||||
Addr string
|
||||
LogsLocation string
|
||||
ClientConfig *gossh.ClientConfig
|
||||
LoggingMode string
|
||||
Addr string
|
||||
LogsLocation string
|
||||
ClientConfig *gossh.ClientConfig
|
||||
LoggingMode string
|
||||
ProxyProtocol string
|
||||
ClientIP net.Addr
|
||||
}
|
||||
|
||||
// dialWithProxyProto dials the target host with optional PROXY protocol header
|
||||
// When PROXY protocol is enabled, the header is sent before the SSH handshake
|
||||
func dialWithProxyProto(network, addr string, config *gossh.ClientConfig, proxyProtocol string, clientIP net.Addr) (*gossh.Client, error) {
|
||||
// If Proxy Protocol is enabled, send the header before SSH handshake
|
||||
if proxyProtocol == "v1" || proxyProtocol == "v2" {
|
||||
// Parse target address
|
||||
targetAddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse source address (client IP) or use localhost if unknown
|
||||
var srcAddr net.Addr
|
||||
if clientIP != nil {
|
||||
srcAddr = clientIP
|
||||
} else {
|
||||
srcAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:0")
|
||||
}
|
||||
|
||||
// Create a raw TCP connection
|
||||
conn, err := net.Dial(network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create a PROXY protocol header
|
||||
header := proxyproto.HeaderProxyFromAddrs(0, srcAddr, targetAddr)
|
||||
|
||||
// Set protocol version if specified
|
||||
if proxyProtocol == "v1" {
|
||||
header.Version = 1
|
||||
} else {
|
||||
header.Version = 2
|
||||
}
|
||||
|
||||
// Write PROXY protocol header before SSH handshake
|
||||
_, err = header.WriteTo(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create SSH client connection on top of the TCP connection (with PROXY header already sent)
|
||||
// The SSH handshake will now continue after the PROXY header
|
||||
c, chans, reqs, err := gossh.NewClientConn(conn, addr, config)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return a client from the connection
|
||||
return gossh.NewClient(c, chans, reqs), nil
|
||||
}
|
||||
|
||||
// Without Proxy Protocol - use standard dial
|
||||
return gossh.Dial(network, addr, config)
|
||||
}
|
||||
|
||||
func multiChannelHandler(conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context, configs []sessionConfig, sessionID uint) error {
|
||||
@@ -37,7 +99,8 @@ func multiChannelHandler(conn *gossh.ServerConn, newChan gossh.NewChannel, ctx s
|
||||
for _, config := range configs {
|
||||
var client *gossh.Client
|
||||
if lastClient == nil {
|
||||
client, err = gossh.Dial("tcp", config.Addr, config.ClientConfig)
|
||||
// First connection - use dialWithProxyProto if enabled
|
||||
client, err = dialWithProxyProto("tcp", config.Addr, config.ClientConfig, config.ProxyProtocol, config.ClientIP)
|
||||
} else {
|
||||
rconn, err := lastClient.Dial("tcp", config.Addr)
|
||||
if err != nil {
|
||||
@@ -74,11 +137,12 @@ func multiChannelHandler(conn *gossh.ServerConn, newChan gossh.NewChannel, ctx s
|
||||
return nil
|
||||
}
|
||||
|
||||
// go through all the hops
|
||||
// go through all hops
|
||||
for _, config := range configs {
|
||||
var client *gossh.Client
|
||||
if lastClient == nil {
|
||||
client, err = gossh.Dial("tcp", config.Addr, config.ClientConfig)
|
||||
// First connection - use dialWithProxyProto if enabled
|
||||
client, err = dialWithProxyProto("tcp", config.Addr, config.ClientConfig, config.ProxyProtocol, config.ClientIP)
|
||||
} else {
|
||||
rconn, err := lastClient.Dial("tcp", config.Addr)
|
||||
if err != nil {
|
||||
|
||||
@@ -728,6 +728,7 @@ GLOBAL OPTIONS:
|
||||
cli.StringFlag{Name: "key, k", Usage: "`KEY` to use for authentication"},
|
||||
cli.StringFlag{Name: "hop, o", Usage: "Hop to use for connecting to the server"},
|
||||
cli.StringFlag{Name: "logging, l", Usage: "Logging mode (disabled, input, everything)"},
|
||||
cli.StringFlag{Name: "proxy-protocol", Usage: "Proxy protocol version (ssh: default, v1, v2)"},
|
||||
cli.StringSliceFlag{Name: "group, g", Usage: "Assigns the host to `HOSTGROUPS` (default: \"default\")"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
@@ -775,6 +776,12 @@ GLOBAL OPTIONS:
|
||||
if c.String("logging") != "" {
|
||||
host.Logging = c.String("logging")
|
||||
}
|
||||
if proxyProtocol := c.String("proxy-protocol"); proxyProtocol != "" || proxyProtocol == "ssh" {
|
||||
if !dbmodels.ValidProxyProtocol(proxyProtocol) {
|
||||
return fmt.Errorf("invalid proxy protocol version: %q (must be v1 or v2)", proxyProtocol)
|
||||
}
|
||||
host.ProxyProtocol = proxyProtocol
|
||||
}
|
||||
// FIXME: check if name already exists
|
||||
|
||||
if _, err := govalidator.ValidateStruct(host); err != nil {
|
||||
@@ -882,7 +889,7 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
|
||||
table := tablewriter.NewWriter(s)
|
||||
table.SetHeader([]string{"ID", "Name", "URL", "Key", "Groups", "Updated", "Created", "Comment", "Hop", "Logging"})
|
||||
table.SetHeader([]string{"ID", "Name", "URL", "Key", "Groups", "Updated", "Created", "Comment", "Hop", "Logging", "ProxyProtocol"})
|
||||
table.SetBorder(false)
|
||||
table.SetCaption(true, fmt.Sprintf("Total: %d hosts.", len(hosts)))
|
||||
for _, host := range hosts {
|
||||
@@ -908,6 +915,11 @@ GLOBAL OPTIONS:
|
||||
} else {
|
||||
hop = ""
|
||||
}
|
||||
|
||||
var protocol string = "ssh"
|
||||
if len(host.ProxyProtocol) != 0 {
|
||||
protocol = host.ProxyProtocol
|
||||
}
|
||||
table.Append([]string{
|
||||
fmt.Sprintf("%d", host.ID),
|
||||
host.Name,
|
||||
@@ -919,6 +931,7 @@ GLOBAL OPTIONS:
|
||||
host.Comment,
|
||||
hop,
|
||||
host.Logging,
|
||||
protocol,
|
||||
//FIXME: add some stats about last access time etc
|
||||
})
|
||||
}
|
||||
@@ -951,6 +964,7 @@ GLOBAL OPTIONS:
|
||||
cli.StringFlag{Name: "key, k", Usage: "Link a `KEY` to use for authentication"},
|
||||
cli.StringFlag{Name: "hop, o", Usage: "Change the hop to use for connecting to the server"},
|
||||
cli.StringFlag{Name: "logging, l", Usage: "Logging mode (disabled, input, everything)"},
|
||||
cli.StringFlag{Name: "proxy-protocol, p", Usage: "Proxy protocol version (ssh, v1, v2)"},
|
||||
cli.BoolFlag{Name: "unset-hop", Usage: "Remove the hop set for this host"},
|
||||
cli.StringSliceFlag{Name: "assign-group, g", Usage: "Assign the host to a new `HOSTGROUPS`"},
|
||||
cli.StringSliceFlag{Name: "unassign-group", Usage: "Unassign the host from a `HOSTGROUPS`"},
|
||||
@@ -1024,6 +1038,17 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
}
|
||||
|
||||
// proxy-protocol
|
||||
if proxyProtocol := c.String("proxy-protocol"); proxyProtocol != "" || proxyProtocol == "ssh" {
|
||||
if !dbmodels.ValidProxyProtocol(proxyProtocol) {
|
||||
return fmt.Errorf("invalid proxy protocol version: %q (must be v1 or v2)", proxyProtocol)
|
||||
}
|
||||
if err := model.Update("proxy_protocol", proxyProtocol).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// remove the hop
|
||||
if c.Bool("unset-hop") {
|
||||
var hopHost dbmodels.Host
|
||||
@@ -1640,11 +1665,15 @@ GLOBAL OPTIONS:
|
||||
name = c.String("name")
|
||||
}
|
||||
|
||||
r, err := randStringBytes(16)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user := dbmodels.User{
|
||||
Name: name,
|
||||
Email: email,
|
||||
Comment: c.String("comment"),
|
||||
InviteToken: randStringBytes(16),
|
||||
InviteToken: r,
|
||||
}
|
||||
|
||||
if _, err := govalidator.ValidateStruct(user); err != nil {
|
||||
|
||||
@@ -141,6 +141,8 @@ func ChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh
|
||||
ClientConfig: clientConfig,
|
||||
LogsLocation: actx.logsLocation,
|
||||
LoggingMode: currentHost.Logging,
|
||||
ProxyProtocol: currentHost.ProxyProtocol,
|
||||
ClientIP: conn.RemoteAddr(),
|
||||
}}, sessionConfigs...)
|
||||
if currentHost.HopID != 0 {
|
||||
var newHost dbmodels.Host
|
||||
|
||||
@@ -58,12 +58,13 @@ type Host struct {
|
||||
URL string `valid:"optional"`
|
||||
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"` // SSHKey used to connect by the client
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:1000" valid:"optional"`
|
||||
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Comment string `valid:"optional"`
|
||||
Logging string `valid:"optional,host_logging_mode"`
|
||||
Hop *Host
|
||||
HopID uint
|
||||
HostKey []byte `sql:"size:1000" valid:"optional"`
|
||||
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Comment string `valid:"optional"`
|
||||
Logging string `valid:"optional,host_logging_mode"`
|
||||
Hop *Host
|
||||
HopID uint
|
||||
ProxyProtocol string `gorm:"column:proxy_protocol;default:''" valid:"optional"`
|
||||
}
|
||||
|
||||
// UserKey defines a user public key used by sshportal to identify the user
|
||||
@@ -321,6 +322,16 @@ func (host *Host) ClientConfig(hk gossh.HostKeyCallback) (*gossh.ClientConfig, e
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// ValidProxyProtocol returns true if the ProxyProtocol value is valid
|
||||
func ValidProxyProtocol(value string) bool {
|
||||
switch value {
|
||||
case "", "v1", "v2":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// SSHKey helpers
|
||||
|
||||
func SSHKeysPreload(db *gorm.DB) *gorm.DB {
|
||||
|
||||
33
server.go
33
server.go
@@ -14,10 +14,12 @@ import (
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"moul.io/sshportal/pkg/bastion"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/pires/go-proxyproto"
|
||||
"github.com/urfave/cli"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"moul.io/sshportal/pkg/bastion"
|
||||
)
|
||||
|
||||
type serverConfig struct {
|
||||
@@ -28,19 +30,21 @@ type serverConfig struct {
|
||||
debug, demo bool
|
||||
idleTimeout time.Duration
|
||||
aclCheckCmd string
|
||||
proxyProtocol bool
|
||||
}
|
||||
|
||||
func parseServerConfig(c *cli.Context) (*serverConfig, error) {
|
||||
ret := &serverConfig{
|
||||
aesKey: c.String("aes-key"),
|
||||
dbDriver: c.String("db-driver"),
|
||||
dbURL: c.String("db-conn"),
|
||||
bindAddr: c.String("bind-address"),
|
||||
debug: c.Bool("debug"),
|
||||
demo: c.Bool("demo"),
|
||||
logsLocation: c.String("logs-location"),
|
||||
idleTimeout: c.Duration("idle-timeout"),
|
||||
aclCheckCmd: c.String("acl-check-cmd"),
|
||||
aesKey: c.String("aes-key"),
|
||||
dbDriver: c.String("db-driver"),
|
||||
dbURL: c.String("db-conn"),
|
||||
bindAddr: c.String("bind-address"),
|
||||
debug: c.Bool("debug"),
|
||||
demo: c.Bool("demo"),
|
||||
logsLocation: c.String("logs-location"),
|
||||
idleTimeout: c.Duration("idle-timeout"),
|
||||
aclCheckCmd: c.String("acl-check-cmd"),
|
||||
proxyProtocol: !c.Bool("no-proxy-protocol"), // Default to true (enabled)
|
||||
}
|
||||
switch len(ret.aesKey) {
|
||||
case 0, 16, 24, 32:
|
||||
@@ -83,7 +87,7 @@ func dbConnect(c *serverConfig, config gorm.Option) (*gorm.DB, error) {
|
||||
func server(c *serverConfig) (err error) {
|
||||
// configure db logging
|
||||
|
||||
db, err := dbConnect(c, &gorm.Config{
|
||||
db, _ := dbConnect(c, &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
sqlDB, err := db.DB()
|
||||
@@ -110,6 +114,13 @@ func server(c *serverConfig) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// wrap listener with Proxy Protocol support if enabled
|
||||
// When enabled, listener auto-detects: PROXY v1, PROXY v2, or no PROXY (regular SSH)
|
||||
if c.proxyProtocol {
|
||||
ln = &proxyproto.Listener{Listener: ln}
|
||||
log.Printf("Proxy Protocol auto-detection enabled (accepts v1, v2, or plain SSH)")
|
||||
}
|
||||
|
||||
// configure server
|
||||
srv := &ssh.Server{
|
||||
Addr: c.bindAddr,
|
||||
|
||||
Reference in New Issue
Block a user