diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c59895c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM centurylink/ca-certs + +COPY goStatic / +ENTRYPOINT ["/goStatic"] \ No newline at end of file diff --git a/Makefile b/Makefile index 16f92af..bba94c9 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ default: static static: - docker run --rm -v $(pwd):/src centurylink/golang-builder \ No newline at end of file + docker run --rm -v $(shell pwd):/src centurylink/golang-builder \ No newline at end of file diff --git a/README.md b/README.md index 52a6b28..ad11fb9 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,42 @@ A really small static web server for Docker ### The goal My goal is to create to smallest docker container for my web static files. The advantage of Go is that you can generate a fully static binary, so that you don't need anything else. +### Features + * Web server build for Docker + * HTTPS by default + * Can generate certificate on his onw + * Light container + * More security than official images (see below) + * Log enabled + ### Why? -Because the official Golang image is wayyyy to big. (around 1/2Gb as you can see below) +Because the official Golang image is wayyyy to big (around 1/2Gb as you can see below) and could be unsecure. [![](https://badge.imagelayers.io/golang:latest.svg)](https://imagelayers.io/?images=golang:latest 'Get your own badge on imagelayers.io') For me, the whole point of containers is to have a light container... Many links should provide you with additionnal info to see my point of view: + * [Over 30% of Official Images in Docker Hub Contain High Priority Security Vulnerabilities](http://www.banyanops.com/blog/analyzing-docker-hub/) * [Create The Smallest Possible Docker Container](http://blog.xebia.com/2014/07/04/create-the-smallest-possible-docker-container/) * [Building Docker Images for Static Go Binaries](https://medium.com/@kelseyhightower/optimizing-docker-images-for-static-binaries-b5696e26eb07) * [Small Docker Images For Go Apps](http://www.centurylinklabs.com/small-docker-images-for-go-apps/) -### What are you using? +### How to use +``` +// HTTPS server +docker run -d -p 443:8043 -v path/to/website:/srv/http pierrezemb/goStatic +// HTTP server +docker run -d -p 80:8043 -v path/to/website:/srv/http pierrezemb/goStatic --forceHTTP +``` -I'm using [echo](http://echo.labstack.com/) as a micro web framework because he has great performance, and [golang-builder](https://github.com/CenturyLinkLabs/golang-builder) to generate the static binary. +### Wow, such container! What are you using? + +I'm using [echo](http://echo.labstack.com/) as a micro web framework because he has great performance, and [golang-builder](https://github.com/CenturyLinkLabs/golang-builder) to generate the static binary (command line in the makefile) + +I'm also using the centurylink/ca-certs image instead of the scratch image to avoid this error: + +``` +x509: failed to load system roots and no roots provided +``` + +The centurylink/ca-certs image is simply the scratch image with the most common root CA certificates pre-installed. The resulting image is only 258 kB which is still a good starting point for creating your own minimal images. \ No newline at end of file diff --git a/goStatic b/goStatic new file mode 100755 index 0000000..295aac7 Binary files /dev/null and b/goStatic differ diff --git a/main.go b/main.go index cfb347f..91eeac1 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,8 @@ package main // import "github.com/PierreZ/goStatic" import ( + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" @@ -8,23 +10,35 @@ import ( "encoding/pem" "flag" "fmt" - "io/ioutil" "log" "math/big" + "net" "os" "strconv" + "strings" "time" "github.com/labstack/echo" mw "github.com/labstack/echo/middleware" ) -func main() { +var ( // Def & parsing of flags - portPtr := flag.Int("p", 8080, "The proxy listening port") - pathPtr := flag.String("path for static file", "/var/www", "The path for the static files") - crtPtr := flag.String("crt", "/etc/ssl/server", "The path and the name whithout extension for your CRT/key for TLS. Example: /etc/ssl/server for /etc/ssl/server.{crt,key}") - HTTPPtr := flag.Bool("forceHTTP", false, "Forcing HTTP and not HTTPS") + portPtr = flag.Int("p", 8043, "The listening port") + pathPtr = flag.String("static", "/srv/http", "The path for the static files") + crtPtr = flag.String("crt", "/etc/ssl/server", "Folder for server.pem and key.pem") + HTTPPtr = flag.Bool("forceHTTP", false, "Forcing HTTP and not HTTPS") + + // var for cert + host = flag.String("host", "", "Comma-separated hostnames and IPs to generate a certificate for") + validFrom = flag.String("start-date", "", "Creation date formatted as Jan 1 15:04:05 2011") + validFor = flag.Duration("duration", 365*24*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, P384, P521") +) + +func main() { flag.Parse() @@ -45,54 +59,141 @@ func main() { // Start server with unsecure HTTP // or with awesome TLS if *HTTPPtr { + log.Println("Starting serving", *pathPtr, "on", *portPtr) e.Run(port) } else { - if *crtPtr != "/etc/ssl/server" { + // Check for cert + _, err := os.Stat(*crtPtr + "cert.pem") + if err != nil { + log.Println("Cert not found, generating .pem and .crt file") generateCert(*crtPtr) } - e.RunTLS(port, *pathPtr+".pem", *pathPtr+".crt") + log.Println("Starting serving", *pathPtr, "on", *portPtr) + e.RunTLS(port, *crtPtr+"cert.pem", *crtPtr+"key.pem") } } -// Mostly based on https://www.socketloop.com/tutorials/golang-create-x509-certificate-private-and-public-keys +// Based on https://golang.org/src/crypto/tls/generate_cert.go func generateCert(path string) { + var priv interface{} + var err error + switch *ecdsaCurve { + case "": + 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: + fmt.Fprintf(os.Stderr, "Unrecognized elliptic curve: %q", *ecdsaCurve) + os.Exit(1) + } + if err != nil { + log.Fatalf("failed to generate private key: %s", err) + } - template := &x509.Certificate{ - IsCA: true, - BasicConstraintsValid: true, - SubjectKeyId: []byte{1, 2, 3}, - SerialNumber: big.NewInt(1234), + 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 { + fmt.Fprintf(os.Stderr, "Failed to parse creation date: %s\n", err) + os.Exit(1) + } + } + + notAfter := notBefore.Add(*validFor) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + log.Fatalf("failed to generate serial number: %s", err) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, Subject: pkix.Name{ - Country: []string{"Earth"}, Organization: []string{"Hooli"}, }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(5, 5, 5), - // see http://golang.org/pkg/crypto/x509/#KeyUsage - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, } - // generate private key - privatekey, err := rsa.GenerateKey(rand.Reader, 4096) + 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 { - log.Println(err) + log.Fatalf("Failed to create certificate: %s", err) } - // save cert - ioutil.WriteFile(path+".crt", cert, 0777) - fmt.Println("certificate saved to", path+".crt") + certOut, err := os.Create(path + "cert.pem") + if err != nil { + log.Fatalf("failed to open cert.pem for writing: %s", err) + } + pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + certOut.Close() + log.Print("written cert.pem\n") - // this will create plain text PEM file. - pemfile, _ := os.Create(path + ".pem") - var pemkey = &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(privatekey)} - pem.Encode(pemfile, pemkey) - pemfile.Close() + keyOut, err := os.OpenFile(path+"key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + log.Print("failed to open key.pem for writing:", err) + return + } + pem.Encode(keyOut, pemBlockForKey(priv)) + keyOut.Close() + log.Print("written key.pem\n") +} + +// Based on https://golang.org/src/crypto/tls/generate_cert.go +func publicKey(priv interface{}) interface{} { + switch k := priv.(type) { + case *rsa.PrivateKey: + return &k.PublicKey + case *ecdsa.PrivateKey: + return &k.PublicKey + default: + return nil + } +} + +// Based on https://golang.org/src/crypto/tls/generate_cert.go +func pemBlockForKey(priv interface{}) *pem.Block { + switch k := priv.(type) { + case *rsa.PrivateKey: + return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)} + case *ecdsa.PrivateKey: + b, err := x509.MarshalECPrivateKey(k) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to marshal ECDSA private key: %v", err) + os.Exit(2) + } + return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b} + default: + return nil + } }