Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add authentication-key-request-url option #247

Merged
merged 2 commits into from
Oct 21, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Switch to using JSON body in request and include username & remote ad…
…dress of client.
  • Loading branch information
rjobanp committed Oct 17, 2022
commit bff79c79f2638b5784b4025ff2ff68577c31fc06
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,11 +316,17 @@ Flags:
--append-user-to-subdomain Append the SSH user to the subdomain. This is useful in multitenant environments
--append-user-to-subdomain-separator string The token to use for separating username and subdomain selection in a virtualhost (default "-")
--authentication Require authentication for the SSH service (default true)
--authentication-key-request-timeout duration Duration to wait for a response from the authentication key request (default 5s)
-v, --authentication-key-request-url string A url to validate public keys for public key authentication.
sish will make an HTTP POST request to this URL with a JSON body containing an
OpenSSH 'authorized key' formatted public key, username,
and ip address. E.g.:
{"auth_key": string, "user": string, "remote_addr": string}
A response with status code 200 indicates approval of the auth key
-k, --authentication-keys-directory string Directory where public keys for public key authentication are stored.
sish will watch this directory and automatically load new keys and remove keys
from the authentication list (default "deploy/pubkeys/")
--authentication-keys-directory-watch-interval duration The interval to poll for filesystem changes for SSH keys (default 200ms)
-v, --authentication-key-request-url A url to validate public keys for public key authentication. sish will make an HTTP POST request to this URL with a body containing an OpenSSH 'authorized key' formatted public key. A response with status code 200 indicates approval of the auth key
-u, --authentication-password string Password to use for SSH server password authentication
--banned-aliases string A comma separated list of banned aliases that users are unable to bind
-o, --banned-countries string A comma separated list of banned countries. Applies to HTTP, TCP, and SSH connections
Expand Down
2 changes: 1 addition & 1 deletion cmd/sish.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func init() {
rootCmd.PersistentFlags().StringP("private-keys-directory", "l", "deploy/keys", "The location of other SSH server private keys. sish will add these as valid auth methods for SSH. Note, these need to be unencrypted OR use the private-key-passphrase")
rootCmd.PersistentFlags().StringP("authentication-password", "u", "", "Password to use for SSH server password authentication")
rootCmd.PersistentFlags().StringP("authentication-keys-directory", "k", "deploy/pubkeys/", "Directory where public keys for public key authentication are stored.\nsish will watch this directory and automatically load new keys and remove keys\nfrom the authentication list")
rootCmd.PersistentFlags().StringP("authentication-key-request-url", "v", "", "A url to validate public keys for public key authentication.\nsish will make an HTTP POST request to this URL with a body containing an\nOpenSSH 'authorized key' formatted public key.\nA response with status code 200 indicates approval of the auth key")
rootCmd.PersistentFlags().StringP("authentication-key-request-url", "v", "", "A url to validate public keys for public key authentication.\nsish will make an HTTP POST request to this URL with a JSON body containing an\nOpenSSH 'authorized key' formatted public key, username,\nand ip address. E.g.:\n{\"auth_key\": string, \"user\": string, \"remote_addr\": string}\nA response with status code 200 indicates approval of the auth key")
rootCmd.PersistentFlags().StringP("port-bind-range", "n", "0,1024-65535", "Ports or port ranges that sish will allow to be bound when a user attempts to use TCP forwarding")
rootCmd.PersistentFlags().StringP("proxy-protocol-version", "q", "1", "What version of the proxy protocol to use. Can either be 1, 2, or userdefined.\nIf userdefined, the user needs to add a command to SSH called proxyproto=version (ie proxyproto=1)")
rootCmd.PersistentFlags().StringP("proxy-protocol-policy", "", "use", "What to do with the proxy protocol header. Can be use, ignore, reject, or require")
Expand Down
62 changes: 53 additions & 9 deletions utils/authentication_key_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"crypto/rand"
"crypto/rsa"
"encoding/json"
"io"
"log"
"net"
Expand All @@ -29,34 +30,57 @@ func MakeTestKeys(numKeys int) []*rsa.PrivateKey {
return testKeys
}

type AuthRequestBody struct {
PubKey string `json:"auth_key"`
UserName string `json:"user"`
RemoteAddr string `json:"remote_addr"`
}

// PubKeyHttpHandler returns a http handler function which validates an
// OpenSSH authorized-keys formatted public key against a slice of
// slice authorized keys.
func PubKeyHttpHandler(validPublicKeys *[]rsa.PublicKey) func(w http.ResponseWriter, r *http.Request) {
func PubKeyHttpHandler(validPublicKeys *[]rsa.PublicKey, validUsernames *[]string) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pubKey, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
parsedKey, _, _, _, err := ssh.ParseAuthorizedKey(pubKey)
var reqBody AuthRequestBody
err = json.Unmarshal(pubKey, &reqBody)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
parsedKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(reqBody.PubKey))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
marshalled := parsedKey.Marshal()
keyMatch := false
usernameMatch := false
for _, key := range *validPublicKeys {
authorizedKey, err := ssh.NewPublicKey(&key)
if err != nil {
log.Print("Error parsing authorized public key", err)
continue
}
if bytes.Equal(authorizedKey.Marshal(), marshalled) {
w.WriteHeader(http.StatusOK)
return
keyMatch = true
break
}
}
for _, username := range *validUsernames {
if reqBody.UserName == username {
usernameMatch = true
}
}
w.WriteHeader(http.StatusUnauthorized)
if keyMatch && usernameMatch {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusUnauthorized)
}
}
}

Expand Down Expand Up @@ -99,35 +123,54 @@ func TestAuthenticationKeyRequest(t *testing.T) {

testCases := []struct {
clientPrivateKey *rsa.PrivateKey
clientUser string
validPublicKeys []rsa.PublicKey
validUsernames []string
expectSuccessAuth bool
overrideHttpUrl string
}{
// valid key, should succeed auth
{
clientPrivateKey: testKeys[0],
clientUser: "ubuntu",
validPublicKeys: []rsa.PublicKey{testKeys[0].PublicKey},
validUsernames: []string{"ubuntu"},
expectSuccessAuth: true,
overrideHttpUrl: "",
},
// invalid key, should be rejected
{
clientPrivateKey: testKeys[0],
clientUser: "ubuntu",
validPublicKeys: []rsa.PublicKey{testKeys[1].PublicKey, testKeys[2].PublicKey},
validUsernames: []string{"ubuntu"},
expectSuccessAuth: false,
overrideHttpUrl: "",
},
// invalid username, should be rejected
{
clientPrivateKey: testKeys[0],
clientUser: "windows",
validPublicKeys: []rsa.PublicKey{testKeys[0].PublicKey},
validUsernames: []string{"ubuntu"},
expectSuccessAuth: false,
overrideHttpUrl: "",
},
// no http service listening on server url, should be rejected
{
clientPrivateKey: testKeys[0],
validPublicKeys: []rsa.PublicKey{},
clientUser: "ubuntu",
validPublicKeys: []rsa.PublicKey{testKeys[0].PublicKey},
validUsernames: []string{"ubuntu"},
expectSuccessAuth: false,
overrideHttpUrl: "http:https://localhost:61234",
},
// invalid http url, should be rejected
{
clientPrivateKey: testKeys[0],
validPublicKeys: []rsa.PublicKey{},
clientUser: "ubuntu",
validPublicKeys: []rsa.PublicKey{testKeys[0].PublicKey},
validUsernames: []string{"ubuntu"},
expectSuccessAuth: false,
overrideHttpUrl: "notarealurl",
},
Expand All @@ -136,7 +179,7 @@ func TestAuthenticationKeyRequest(t *testing.T) {
for caseIdx, c := range testCases {
if c.overrideHttpUrl == "" {
// start an http server that will validate against the specified public keys
httpSrv := httptest.NewServer(http.HandlerFunc(PubKeyHttpHandler(&c.validPublicKeys)))
httpSrv := httptest.NewServer(http.HandlerFunc(PubKeyHttpHandler(&c.validPublicKeys, &c.validUsernames)))
defer httpSrv.Close()

// set viper to this http server URL as the auth request url it will
Expand All @@ -155,7 +198,7 @@ func TestAuthenticationKeyRequest(t *testing.T) {
successAuth := make(chan bool)
go HandleSSHConn(sshListener, &successAuth)

// // attempt to connect to the ssh server using the specified private key
// attempt to connect to the ssh server using the specified private key
signer, err := ssh.NewSignerFromKey(c.clientPrivateKey)
if err != nil {
t.Error(err)
Expand All @@ -165,6 +208,7 @@ func TestAuthenticationKeyRequest(t *testing.T) {
ssh.PublicKeys(signer),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
User: c.clientUser,
}
t.Log(clientConfig)

Expand Down
18 changes: 14 additions & 4 deletions utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/json"
"encoding/pem"
"fmt"
"io"
Expand Down Expand Up @@ -485,7 +486,7 @@ func GetSSHConfig() *ssh.ServerConfig {
// Allow validation of public keys via a sub-request to another service
authUrl := viper.GetString("authentication-key-request-url")
if authUrl != "" {
validKey, err := checkAuthenticationKeyRequest(authUrl, authKey)
validKey, err := checkAuthenticationKeyRequest(authUrl, authKey, c.RemoteAddr(), c.User())
if err != nil {
log.Printf("Error calling authentication URL %s: %s\n", authUrl, err)
}
Expand All @@ -512,17 +513,26 @@ func GetSSHConfig() *ssh.ServerConfig {
// checkAuthenticationKeyRequest makes an HTTP POST request to the specified url with
// the provided ssh public key in OpenSSH 'authorized keys' format to validate
// whether it should be accepted.
func checkAuthenticationKeyRequest(authUrl string, authKey []byte) (bool, error) {
func checkAuthenticationKeyRequest(authUrl string, authKey []byte, addr net.Addr, user string) (bool, error) {
parsedUrl, err := url.ParseRequestURI(authUrl)
if err != nil || !parsedUrl.IsAbs() {
if err != nil {
return false, fmt.Errorf("error parsing url %s", err)
}

c := &http.Client{
Timeout: viper.GetDuration("authentication-key-request-timeout"),
}
urlS := parsedUrl.String()
res, err := c.Post(urlS, "text/plain", bytes.NewBuffer(authKey))
reqBodyMap := map[string]string{
"auth_key": string(authKey),
"remote_addr": addr.String(),
"user": user,
}
reqBody, err := json.Marshal(reqBodyMap)
if err != nil {
return false, fmt.Errorf("error jsonifying request body")
}
res, err := c.Post(urlS, "application/json", bytes.NewBuffer(reqBody))
if err != nil {
return false, err
}
Expand Down