mirror of
https://github.com/boringproxy/boringproxy.git
synced 2025-02-25 18:55:29 -06:00
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:
parent
95ab97f043
commit
8a37355bb6
107
api.go
Normal file
107
api.go
Normal 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)
|
||||
})
|
||||
}
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user