diff --git a/README.md b/README.md index 5a82799..989c040 100644 --- a/README.md +++ b/README.md @@ -6,20 +6,36 @@ All URLs in the OpenStack catalog are rewritten to point to the proxy itself, wh ## Use locally -Download the binary for linux64 on this repository's [release page](https://github.com/pierreprinetti/openstack-mitm/releases) or build it with `go build ./cmd/osp-mitm`. +Build with `go build ./cmd/osp-mitm`. -**Required configuration:** -* **--remote-authurl**: URL of the remote OpenStack Keystone. -* **--proxy-url**: URL the proxy will be reachable at. +`osp-mitm` will parse a `clouds.yaml` file at the known locations, similar to what python-openstackclient does. -**Optional configuration:** -* **--remote-cacert**: path of the local PEM-encoded file containing the CA for the remote certificate. -* **--insecure**: skip TLS verification. +By default the server will listen on localhost on port 13000. + +**Configuration:** +* **--url**: URL osp-mitm will be reachable at. Default: `http://locahost:13000` +* **--cert**: path of the local PEM-encoded HTTPS certificate file. Mandatory if the scheme of --url is HTTPS. +* **--key**: path of the local PEM-encoded HTTPS certificate key file. Mandatory if the scheme of --url is HTTPS. +* **-o**: If provided, a new clouds.yaml that points to osp-mitm is created at that location. + +## Examples + +Local server: +```shell +export OS_CLOUD=openstack +./osp-mitm -o mitm-clouds.yaml +``` +```shell +export OS_CLIENT_CONFIG_FILE=./mitm-clouds.yaml +openstack server list +``` + +On the network, with HTTPS: -Example: ```shell ./osp-mitm \ - --remote-authurl https://openstack.example.com:13000/v3 \ - --remote-cacert /var/openstack/cert.pem \ - --proxy-url https://localhost:15432' + --url https://myserver.example.com:13000 \ + --cert /var/run/osp-cert.pem \ + --key /var/run/osp-key.pem' \ + -o mitm-clouds.yaml ``` diff --git a/cmd/osp-mitm/generate_cert.go b/cmd/osp-mitm/generate_cert.go deleted file mode 100644 index 0251275..0000000 --- a/cmd/osp-mitm/generate_cert.go +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file at -// https://github.com/golang/go/blob/master/LICENSE - -// Generate a self-signed X.509 certificate for a TLS server. Outputs to -// 'cert.pem' and 'key.pem' and will overwrite existing files. - -package main - -import ( - "crypto/ecdsa" - "crypto/ed25519" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "flag" - "fmt" - "math/big" - "net" - "os" - "strings" - "time" -) - -var ( - validFrom = flag.String("start-date", "", "Creation date formatted as Jan 1 15:04:05 2011") - validFor = flag.Duration("duration", 5*time.Hour, "Duration that certificate is valid for") - isCA = flag.Bool("ca", false, "whether this cert should be its own Certificate Authority") - rsaBits = flag.Int("rsa-bits", 2048, "Size of RSA key to generate. Ignored if --ecdsa-curve is set") - ecdsaCurve = flag.String("ecdsa-curve", "", "ECDSA curve to use to generate a key. Valid values are P224, P256 (recommended), P384, P521") - ed25519Key = flag.Bool("ed25519", false, "Generate an Ed25519 key") -) - -func publicKey(priv interface{}) interface{} { - switch k := priv.(type) { - case *rsa.PrivateKey: - return &k.PublicKey - case *ecdsa.PrivateKey: - return &k.PublicKey - case ed25519.PrivateKey: - return k.Public().(ed25519.PublicKey) - default: - return nil - } -} - -func generateCertificate(host string) error { - if len(host) == 0 { - return fmt.Errorf("Missing required --host parameter") - } - - var priv interface{} - var err error - switch *ecdsaCurve { - case "": - if *ed25519Key { - _, priv, err = ed25519.GenerateKey(rand.Reader) - } else { - priv, err = rsa.GenerateKey(rand.Reader, *rsaBits) - } - case "P224": - priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader) - case "P256": - priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - case "P384": - priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - case "P521": - priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) - default: - return fmt.Errorf("Unrecognized elliptic curve: %q", *ecdsaCurve) - } - if err != nil { - return fmt.Errorf("Failed to generate private key: %v", err) - } - - // ECDSA, ED25519 and RSA subject keys should have the DigitalSignature - // KeyUsage bits set in the x509.Certificate template - keyUsage := x509.KeyUsageDigitalSignature - // Only RSA subject keys should have the KeyEncipherment KeyUsage bits set. In - // the context of TLS this KeyUsage is particular to RSA key exchange and - // authentication. - if _, isRSA := priv.(*rsa.PrivateKey); isRSA { - keyUsage |= x509.KeyUsageKeyEncipherment - } - - var notBefore time.Time - if len(*validFrom) == 0 { - notBefore = time.Now() - } else { - notBefore, err = time.Parse("Jan 2 15:04:05 2006", *validFrom) - if err != nil { - return fmt.Errorf("Failed to parse creation date: %v", err) - } - } - - notAfter := notBefore.Add(*validFor) - - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) - if err != nil { - return fmt.Errorf("Failed to generate serial number: %v", err) - } - - template := x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{ - Organization: []string{"Red Hat Inc."}, - }, - NotBefore: notBefore, - NotAfter: notAfter, - - KeyUsage: keyUsage, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - } - - hosts := strings.Split(host, ",") - for _, h := range hosts { - if ip := net.ParseIP(h); ip != nil { - template.IPAddresses = append(template.IPAddresses, ip) - } else { - template.DNSNames = append(template.DNSNames, h) - } - } - - if *isCA { - template.IsCA = true - template.KeyUsage |= x509.KeyUsageCertSign - } - - derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv) - if err != nil { - return fmt.Errorf("Failed to create certificate: %v", err) - } - - certOut, err := os.Create("cert.pem") - if err != nil { - return fmt.Errorf("Failed to open cert.pem for writing: %v", err) - } - if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { - return fmt.Errorf("Failed to write data to cert.pem: %v", err) - } - if err := certOut.Close(); err != nil { - return fmt.Errorf("Error closing cert.pem: %v", err) - } - // log.Print("wrote cert.pem\n") - - keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - return fmt.Errorf("Failed to open key.pem for writing: %v", err) - } - privBytes, err := x509.MarshalPKCS8PrivateKey(priv) - if err != nil { - return fmt.Errorf("Unable to marshal private key: %v", err) - } - if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil { - return fmt.Errorf("Failed to write data to key.pem: %v", err) - } - if err := keyOut.Close(); err != nil { - return fmt.Errorf("Error closing key.pem: %v", err) - } - // log.Print("wrote key.pem\n") - return nil -} diff --git a/cmd/osp-mitm/main.go b/cmd/osp-mitm/main.go index 358e913..3562d1d 100644 --- a/cmd/osp-mitm/main.go +++ b/cmd/osp-mitm/main.go @@ -16,101 +16,128 @@ package main import ( "crypto/tls" - "crypto/x509" "flag" "log" "net/http" "net/url" "os" + "strings" - "github.com/pierreprinetti/openstack-mitm/pkg/proxy" -) + "github.com/gophercloud/gophercloud/v2/openstack/config/clouds" + "gopkg.in/yaml.v2" -const ( - defaultPort = "5443" + "github.com/pierreprinetti/openstack-mitm/pkg/proxy" ) var ( - proxyURLstring = flag.String("proxy-url", "", "The address this proxy will be reachable at") - osAuthURL = flag.String("remote-authurl", "", "OpenStack entrypoint (OS_AUTH_URL)") - osCaCert = flag.String("remote-cacert", "", "OpenStack CA certificate (OS_CACERT)") - insecure = flag.Bool("insecure", false, "Insecure connection to OpenStack") + mitmURL *url.URL + identityEndpoint string + tlsConfig *tls.Config + + tlsCertPath string + tlsKeyPath string ) func init() { + var ( + mitmURLstring string + outputCloudPath string + ) + + flag.StringVar(&mitmURLstring, "url", "http://localhost:13000", "The address this MITM proxy will be reachable at") + flag.StringVar(&tlsCertPath, "cert", "", "Path to the PEM-encoded TLS certificate") + flag.StringVar(&tlsKeyPath, "key", "", "Path to the PEM-encoded TLS certificate private key") + flag.StringVar(&outputCloudPath, "o", "", "Path of the clouds.yaml file that points to this MITM proxy (optional)") flag.Parse() - var errexit bool - if *proxyURLstring == "" { - errexit = true - log.Print("Missing required parameter: --proxyurl") + var err error + mitmURL, err = url.Parse(mitmURLstring) + if err != nil { + log.Fatalf("Failed to parse the URL (%q): %v", mitmURLstring, err) } - if *osAuthURL == "" { - log.Print("Missing required parameter: --authurl") + authOptions, endpointOptions, parsedTLSConfig, err := clouds.Parse() + if err != nil { + log.Fatalf("Failed to parse clouds.yaml: %v", err) } - if errexit { - log.Fatal("Exiting.") - } -} + identityEndpoint = authOptions.IdentityEndpoint + tlsConfig = parsedTLSConfig -func main() { - var proxyURL *url.URL - { - var err error - proxyURL, err = url.Parse(*proxyURLstring) + if outputCloudPath != "" { + f, err := os.Create(outputCloudPath) if err != nil { - log.Fatal(err) + log.Fatalf("Failed to create clouds.yaml at the given destination (%q):%v", outputCloudPath, err) } - - if proxyURL.Host == "" { - log.Fatal("The --proxyurl parameter is invalid. It should be in the form: 'https://host[:port]'.") + defer func() { + if err := f.Close(); err != nil { + log.Fatalf("Failed to finalize the clouds.yaml file: %v", err) + } + }() + + var authType clouds.AuthType + switch { + case authOptions.Username != "": + authType = clouds.AuthV3Password + case authOptions.ApplicationCredentialID != "": + authType = clouds.AuthV3ApplicationCredential + default: + log.Fatal("Unknown authentication type.") } - if proxyURL.Path != "" { - log.Fatal("The --proxyurl URL should have empty path.") + c := clouds.Clouds{ + Clouds: map[string]clouds.Cloud{ + os.Getenv("OS_CLOUD"): { + Cloud: outputCloudPath, + AuthInfo: &clouds.AuthInfo{ + AuthURL: mitmURL.String(), + Username: authOptions.Username, + UserID: authOptions.UserID, + Password: authOptions.Password, + ApplicationCredentialID: authOptions.ApplicationCredentialID, + ApplicationCredentialName: authOptions.ApplicationCredentialName, + ApplicationCredentialSecret: authOptions.ApplicationCredentialSecret, + UserDomainName: authOptions.DomainName, + UserDomainID: authOptions.DomainID, + }, + AuthType: authType, + RegionName: endpointOptions.Region, + EndpointType: endpointOptions.Type, + CACertFile: tlsCertPath, + }, + }, } - if proxyURL.Port() == "" { - proxyURL.Host = proxyURL.Hostname() + ":" + defaultPort + if err := yaml.NewEncoder(f).Encode(c); err != nil { + log.Fatalf("Failed to encode the output clouds.yaml: %v", err) } } +} - transport := http.DefaultTransport.(*http.Transport) +func main() { + var p http.Handler + { + var err error + transport := http.DefaultTransport.(*http.Transport) + transport.TLSClientConfig = tlsConfig - if caCertPath := *osCaCert; caCertPath != "" { - b, err := os.ReadFile(caCertPath) + p, err = proxy.NewOpenstackProxy(mitmURL.String(), identityEndpoint, transport) if err != nil { - log.Fatalf("Failed to read the given PEM certificate: %v", err) + log.Fatalf("Failed to build the OpenStack MITM proxy: %v", err) } - certPool := x509.NewCertPool() - if !certPool.AppendCertsFromPEM(b) { - log.Fatal("Failed to parse the given PEM certificate") - } - transport.TLSClientConfig = &tls.Config{RootCAs: certPool} } - if *insecure { - transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: *insecure} - } + listenURL := ":" + mitmURL.Port() - p, err := proxy.NewOpenstackProxy(proxyURL.String(), *osAuthURL, transport) - if err != nil { - panic(err) - } + log.Printf("Proxying to %q", identityEndpoint) + log.Printf("Listening on %q", listenURL) - { - if err := generateCertificate(proxyURL.Hostname()); err != nil { - log.Fatal(err) - } - log.Printf("Certificate correctly generated for %q", proxyURL.Hostname()) + switch strings.ToLower(mitmURL.Scheme) { + case "http": + log.Fatal(http.ListenAndServe(listenURL, p)) + case "https": + log.Fatal(http.ListenAndServeTLS(listenURL, tlsCertPath, tlsKeyPath, p)) + default: + log.Fatalf("Unknown scheme %q", mitmURL.Scheme) } - - log.Printf("Proxying to %q", *osAuthURL) - log.Printf("Listening on %q", proxyURL) - - log.Fatal( - http.ListenAndServeTLS(":"+proxyURL.Port(), "cert.pem", "key.pem", p), - ) } diff --git a/go.mod b/go.mod index 66ad193..8799552 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,9 @@ module github.com/pierreprinetti/openstack-mitm go 1.22 -require github.com/gofrs/uuid/v5 v5.2.0 +require ( + github.com/gofrs/uuid/v5 v5.2.0 + github.com/gophercloud/gophercloud/v2 v2.0.0-rc.3 +) + +require gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum index 831892f..9830f4a 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,8 @@ github.com/gofrs/uuid/v5 v5.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM= github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/gophercloud/gophercloud/v2 v2.0.0-rc.3 h1:Gc1oFIROarJoIcvg63BsXrK6G+DPwYVs5gKlmvV2UC8= +github.com/gophercloud/gophercloud/v2 v2.0.0-rc.3/go.mod h1:ZKbcGNjxFTSaP5wlvtLDdsppllD/UGGvXBPqcjeqA8Y= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=