Implement openssh key management

I had been moving in the direction of implementing a custom SSH
server in golang. That would be pretty easy if using a custom
application protocol, but I want to support tcpip-forward which
looks like it would be a lot more work. It also would be nice to
support generic CLI clients like OpenSSH.

The point of using SSH in the first place is that it's known to be a
solid tunneling solution. To that end, I've decided to rely on
OpenSSH for now, since that program may have tunneled more bits than any
other since the dawn of time. This requires a bit of hackery to generate
SSH keys and place them in authorized_keys (as well as shipping the
private key to the client), but I think this will work well for now.

Plus OpenSSH is already installed on pretty much every server
I'd expect to run boringproxy.
This commit is contained in:
Anders Pitman 2020-10-02 16:57:09 -06:00
parent 95ab97f043
commit 8a37355bb6
3 changed files with 211 additions and 5 deletions

107
api.go Normal file
View File

@ -0,0 +1,107 @@
package main
import (
//"fmt"
//"strings"
"net/http"
"io"
"encoding/json"
)
type Api struct {
auth *Auth
tunMan *TunnelManager
mux *http.ServeMux
}
type CreateTunnelResponse struct {
ServerAddress string `json:"server_address"`
ServerPort int `json:"server_port"`
ServerPublicKey string `json:"server_public_key"`
TunnelPort int `json:"tunnel_port"`
TunnelPrivateKey string `json:"tunnel_private_key"`
}
func NewApi(auth *Auth, tunMan *TunnelManager) *Api {
api := &Api{auth, tunMan, nil}
mux := http.NewServeMux()
mux.Handle("/tunnels", http.StripPrefix("/tunnels", http.HandlerFunc(api.handleTunnels)))
api.mux = mux
return api
}
func (a *Api) ServeHTTP(w http.ResponseWriter, r *http.Request) {
a.mux.ServeHTTP(w, r)
}
func (a *Api) handleTunnels(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "POST":
a.validateSession(http.HandlerFunc(a.handleCreateTunnel)).ServeHTTP(w, r)
default:
w.WriteHeader(405)
w.Write([]byte("Invalid method for /tunnels"))
}
}
func (a *Api) handleCreateTunnel(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
if len(query["domain"]) != 1 {
w.WriteHeader(400)
w.Write([]byte("Invalid domain parameter"))
return
}
domain := query["domain"][0]
port, privKey, err := a.tunMan.CreateTunnel(domain)
if err != nil {
w.WriteHeader(400)
io.WriteString(w, err.Error())
return
}
response := &CreateTunnelResponse{
ServerAddress: "anders.boringproxy.io",
ServerPort: 22,
ServerPublicKey: "",
TunnelPort: port,
TunnelPrivateKey: privKey,
}
responseJson, err := json.MarshalIndent(response, "", " ")
if err != nil {
w.WriteHeader(500)
io.WriteString(w, "Error encoding response")
return
}
w.Write(responseJson)
}
func (a *Api) validateSession(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, err := extractToken("access_token", r)
if err != nil {
w.WriteHeader(401)
w.Write([]byte("No token provided"))
return
}
if !a.auth.Authorized(token) {
w.WriteHeader(403)
w.Write([]byte("Not authorized"))
return
}
h.ServeHTTP(w, r)
})
}

View File

@ -34,7 +34,6 @@ type BoringProxy struct {
tunMan *TunnelManager
adminListener *AdminListener
certConfig *certmagic.Config
sshServer *SshServer
}
func NewBoringProxy() *BoringProxy {
@ -68,11 +67,15 @@ func NewBoringProxy() *BoringProxy {
auth := NewAuth()
sshServer := NewSshServer()
p := &BoringProxy{config, auth, tunMan, adminListener, certConfig}
p := &BoringProxy{config, auth, tunMan, adminListener, certConfig, sshServer}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
p.handleAdminRequest(w, r)
})
api := NewApi(auth, tunMan)
http.Handle("/api/", http.StripPrefix("/api", api))
http.HandleFunc("/", p.handleAdminRequest)
go http.Serve(adminListener, nil)
log.Println("BoringProxy ready")

View File

@ -1,12 +1,19 @@
package main
import (
"fmt"
"strings"
"encoding/json"
"errors"
"github.com/caddyserver/certmagic"
"io/ioutil"
"log"
"sync"
"crypto/rsa"
"crypto/rand"
"encoding/pem"
"crypto/x509"
"golang.org/x/crypto/ssh"
)
type Tunnel struct {
@ -20,6 +27,7 @@ func NewTunnels() Tunnels {
}
type TunnelManager struct {
nextPort int
tunnels Tunnels
mutex *sync.Mutex
certConfig *certmagic.Config
@ -49,8 +57,10 @@ func NewTunnelManager(certConfig *certmagic.Config) *TunnelManager {
}
}
nextPort := 9001
mutex := &sync.Mutex{}
return &TunnelManager{tunnels, mutex, certConfig}
return &TunnelManager{nextPort, tunnels, mutex, certConfig}
}
func (m *TunnelManager) SetTunnel(host string, port int) error {
@ -69,6 +79,35 @@ func (m *TunnelManager) SetTunnel(host string, port int) error {
return nil
}
func (m *TunnelManager) CreateTunnel(domain string) (int, string, error) {
err := m.certConfig.ManageSync([]string{domain})
if err != nil {
log.Println(err)
return 0, "", errors.New("Failed to get cert")
}
m.mutex.Lock()
defer m.mutex.Unlock()
_, exists := m.tunnels[domain]
if exists {
return 0, "", errors.New("Tunnel exists for domain " + domain)
}
port := m.nextPort
m.nextPort += 1
tunnel := &Tunnel{port}
m.tunnels[domain] = tunnel
saveJson(m.tunnels, "tunnels.json")
privKey, err := m.addToAuthorizedKeys(port)
if err != nil {
return 0, "", err
}
return port, privKey, nil
}
func (m *TunnelManager) DeleteTunnel(host string) {
m.mutex.Lock()
delete(m.tunnels, host)
@ -87,3 +126,60 @@ func (m *TunnelManager) GetPort(serverName string) (int, error) {
return tunnel.Port, nil
}
func (m *TunnelManager) addToAuthorizedKeys(port int) (string, error) {
akBytes, err := ioutil.ReadFile("/home/anders/.ssh/authorized_keys")
if err != nil {
return "", err
}
akStr := string(akBytes)
pubKey, privKey, err := MakeSSHKeyPair()
if err != nil {
return "", err
}
options := fmt.Sprintf(`command="echo This key permits tunnels only",permitopen="fakehost:1",permitlisten="localhost:%d"`, port)
pubKeyNoNewline := pubKey[:len(pubKey) - 1]
newAk := fmt.Sprintf("%s%s %s %s%d\n", akStr, options, pubKeyNoNewline, "boringproxy-", port)
err = ioutil.WriteFile("/home/anders/.ssh/authorized_keys", []byte(newAk), 0600)
if err != nil {
return "", err
}
return privKey, nil
}
// Adapted from https://stackoverflow.com/a/34347463/943814
// MakeSSHKeyPair make a pair of public and private keys for SSH access.
// Public key is encoded in the format for inclusion in an OpenSSH authorized_keys file.
// Private Key generated is PEM encoded
func MakeSSHKeyPair() (string, string, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
return "", "", err
}
// generate and write private key as PEM
var privKeyBuf strings.Builder
privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}
if err := pem.Encode(&privKeyBuf, privateKeyPEM); err != nil {
return "", "", err
}
// generate and write public key
pub, err := ssh.NewPublicKey(&privateKey.PublicKey)
if err != nil {
return "", "", err
}
var pubKeyBuf strings.Builder
pubKeyBuf.Write(ssh.MarshalAuthorizedKey(pub))
return pubKeyBuf.String(), privKeyBuf.String(), nil
}