Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions cmd/image-inspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"flag"
"fmt"
"log"

iicmd "github.com/simon3z/image-inspector/pkg/cmd"
Expand All @@ -19,6 +20,7 @@ func main() {
flag.StringVar(&inspectorOptions.DockerCfg, "dockercfg", inspectorOptions.DockerCfg, "Location of the docker configuration file")
flag.StringVar(&inspectorOptions.Username, "username", inspectorOptions.Username, "username for authenticating with the docker registry")
flag.StringVar(&inspectorOptions.PasswordFile, "password-file", inspectorOptions.PasswordFile, "Location of a file that contains the password for authentication with the docker registry")
flag.StringVar(&inspectorOptions.ServerAuthType, "server-auth-type", inspectorOptions.ServerAuthType, fmt.Sprintf("The type of authentication to be used with the image server, possible values are %v", iicmd.ServerAuthOptions))

flag.Parse()

Expand Down
39 changes: 31 additions & 8 deletions pkg/cmd/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ package cmd

import (
"fmt"
server "github.com/simon3z/image-inspector/pkg/imageserver"
)

var (
ServerAuthOptions = []string{
string(server.AllowAll),
string(server.KubernetesToken),
}
)

// ImageInspectorOptions is the main inspector implementation and holds the configuration
Expand All @@ -24,19 +32,22 @@ type ImageInspectorOptions struct {
// PasswordFile is the location of the file containing the password for authentication to the
// docker registry.
PasswordFile string
// ServerAuthType is the type of authentication used to access the server
ServerAuthType string
}

// NewDefaultImageInspectorOptions provides a new ImageInspectorOptions with default values.
func NewDefaultImageInspectorOptions() *ImageInspectorOptions {
return &ImageInspectorOptions{
URI: "unix:///var/run/docker.sock",
Image: "",
DstPath: "",
Serve: "",
Chroot: false,
DockerCfg: "",
Username: "",
PasswordFile: "",
URI: "unix:///var/run/docker.sock",
Image: "",
DstPath: "",
Serve: "",
Chroot: false,
DockerCfg: "",
Username: "",
PasswordFile: "",
ServerAuthType: "None",
}
}

Expand All @@ -57,5 +68,17 @@ func (i *ImageInspectorOptions) Validate() error {
if len(i.Serve) == 0 && i.Chroot {
return fmt.Errorf("Change root can be used only when serving the image through webdav")
}
if !stringInSlice(i.ServerAuthType, ServerAuthOptions) {
return fmt.Errorf("server-auth-type can only be one of %v", ServerAuthOptions)
}
return nil
}

func stringInSlice(str string, list []string) bool {
for _, t := range list {
if t == str {
return true
}
}
return false
}
46 changes: 46 additions & 0 deletions pkg/imageserver/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package imageserver

import (
docker "github.com/fsouza/go-dockerclient"
)

type AuthenticationType string

const (
AllowAll AuthenticationType = "None"
KubernetesToken AuthenticationType = "KubenetesToken"
)

// ImageServer abstracts the serving of image information.
type ImageServer interface {
// ServeImage Serves the image
ServeImage(imageMetadata *docker.Image) error
}

// APIVersions holds a slice of supported API versions.
type APIVersions struct {
// Versions is the supported API versions
Versions []string `json:"versions"`
}

// ImageServerOptions is used to configure an image server.
type ImageServerOptions struct {
// ServePath is the root path/port of serving. ex 0.0.0.0:8080
ServePath string
// HealthzURL is the relative url of the health check. ex /healthz
HealthzURL string
// APIURL is the relative url where the api will be served. ex /api
APIURL string
// APIVersions are the supported API versions.
APIVersions APIVersions
// MetadataURL is the relative url of the metadata content. ex /api/v1/metadata
MetadataURL string
// ContentURL is the relative url of the content. ex /api/v1/content/
ContentURL string
// ImageServeURL is the location that the image is being served from.
// NOTE: if the image server supports a chroot the server implementation will perform
// the chroot based on this URL.
ImageServeURL string
// AuthType is the type of authentication used to access the server
AuthType AuthenticationType
}
151 changes: 151 additions & 0 deletions pkg/imageserver/webdav.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package imageserver

import (
"encoding/json"
"fmt"
"log"
"net/http"
"syscall"

"golang.org/x/net/webdav"

docker "github.com/fsouza/go-dockerclient"

kauthapi "k8s.io/kubernetes/pkg/apis/authorization"
krestclient "k8s.io/kubernetes/pkg/client/restclient"
kclient "k8s.io/kubernetes/pkg/client/unversioned"
)

const (
// CHROOT_SERVE_PATH is the path to server if we are performing a chroot
// this probably does not belong here.
CHROOT_SERVE_PATH = "/"
)

// webdavImageServer implements ImageServer.
type webdavImageServer struct {
opts ImageServerOptions
chroot bool
}

// ensures this always implements the interface or fail compilation.
var _ ImageServer = &webdavImageServer{}

// NewWebdavImageServer creates a new webdav image server.
func NewWebdavImageServer(opts ImageServerOptions, chroot bool) ImageServer {
return &webdavImageServer{
opts: opts,
chroot: chroot,
}
}

// ServeImage Serves the image.
func (s *webdavImageServer) ServeImage(imageMetadata *docker.Image) error {
servePath := s.opts.ImageServeURL
if s.chroot {
if err := syscall.Chroot(s.opts.ImageServeURL); err != nil {
return fmt.Errorf("Unable to chroot into %s: %v\n", s.opts.ImageServeURL, err)
}
servePath = CHROOT_SERVE_PATH
} else {
log.Printf("!!!WARNING!!! It is insecure to serve the image content without changing")
log.Printf("root (--chroot). Absolute-path symlinks in the image can lead to disclose")
log.Printf("information of the hosting system.")
}

log.Printf("Serving image content %s on webdav://%s%s", s.opts.ImageServeURL, s.opts.ServePath, s.opts.ContentURL)

http.HandleFunc(s.opts.HealthzURL, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok\n"))
})

http.HandleFunc(s.opts.APIURL, s.handlerFuncAuth(func(w http.ResponseWriter, r *http.Request) {
body, err := json.MarshalIndent(s.opts.APIVersions, "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(body)
}))

http.HandleFunc(s.opts.MetadataURL, s.handlerFuncAuth(func(w http.ResponseWriter, r *http.Request) {
body, err := json.MarshalIndent(imageMetadata, "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(body)
}))

http.Handle(s.opts.ContentURL, s.newAuthenticatedHandler(&webdav.Handler{
Prefix: s.opts.ContentURL,
FileSystem: webdav.Dir(servePath),
LockSystem: webdav.NewMemLS(),
}))

return http.ListenAndServe(s.opts.ServePath, nil)
}

func (s *webdavImageServer) authenticate(r *http.Request) (bool, error) {
authenticator, ok := map[AuthenticationType]func(*http.Request) (bool, error){
AllowAll: allowAll,
KubernetesToken: kubernetesTokenAuth,
}[s.opts.AuthType]
if !ok {
return false, fmt.Errorf("%s is not a recognize authentication method", s.opts.AuthType)
}
return authenticator(r)
}

func allowAll(r *http.Request) (bool, error) {
return true, nil
}

func kubernetesTokenAuth(r *http.Request) (bool, error) {
conf, err := krestclient.InClusterConfig()
if err != nil {
return false, err
}
conf.BearerToken = r.Header.Get("Authorization")
kc, err := kclient.New(conf)
if err != nil {
return false, err
}
result := &kauthapi.SubjectAccessReview{}
sar := &kauthapi.SubjectAccessReview{}
sar.Kind = "SubjectAccessReview"
sar.APIVersion = "v1"
sar.Spec.ResourceAttributes.Verb = "GET"
sar.Spec.ResourceAttributes.Resource = "images"
err = kc.Get().Resource("subjectAccessReview").Body(sar).Do().Into(result)
if err != nil {
return false, err
}
return result.Status.Allowed, nil
}

func (s *webdavImageServer) handlerFuncAuth(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if allowed, err := s.authenticate(r); allowed && err == nil {
f(w, r)
} else {
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
http.Error(w, "Unauthorazied Access!", http.StatusForbidden)
}
}
}
}

type authenticatedHandler struct {
serveHttp func(http.ResponseWriter, *http.Request)
}

func (ah *authenticatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ah.serveHttp(w, r)
}

func (s *webdavImageServer) newAuthenticatedHandler(h http.Handler) http.Handler {
return &authenticatedHandler{serveHttp: s.handlerFuncAuth(h.ServeHTTP)}
}
Loading