Skip to content

Commit

Permalink
Add --metadata-protection
Browse files Browse the repository at this point in the history
  • Loading branch information
arkadiyt committed Nov 4, 2019
1 parent 39a0038 commit bd4ab8e
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 25 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,7 @@ Usage of kube2iam:
--iam-external-id string Pod annotation key used to retrieve the IAM ExternalId (default "iam.amazonaws.com/external-id")
--insecure Kubernetes server should be accessed without verifying the TLS. Testing only
--iptables Add iptables rule (also requires --host-ip)
--metadata-protection Block metadata requests that don't have a correct AWS User Agent
--log-format string Log format (text/json) (default "text")
--log-level string Log level (default "info")
--metadata-addr string Address for the ec2 metadata (default "169.254.169.254")
Expand Down
1 change: 1 addition & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func addFlags(s *server.Server, fs *pflag.FlagSet) {
fs.BoolVar(&s.Insecure, "insecure", false, "Kubernetes server should be accessed without verifying the TLS. Testing only")
fs.StringVar(&s.MetadataAddress, "metadata-addr", s.MetadataAddress, "Address for the ec2 metadata")
fs.BoolVar(&s.AddIPTablesRule, "iptables", false, "Add iptables rule (also requires --host-ip)")
fs.BoolVar(&s.MetadataProtection, "metadata-protection", false, "Block metadata requests that don't have a correct AWS User Agent")
fs.BoolVar(&s.AutoDiscoverBaseArn, "auto-discover-base-arn", false, "Queries EC2 Metadata to determine the base ARN")
fs.BoolVar(&s.AutoDiscoverDefaultRole, "auto-discover-default-role", false, "Queries EC2 Metadata to determine the default Iam Role and base ARN, cannot be used with --default-role, overwrites any previous setting for --base-role-arn")
fs.StringVar(&s.HostInterface, "host-interface", "docker0", "Host interface for proxying AWS metadata")
Expand Down
2 changes: 1 addition & 1 deletion glide.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 9 additions & 9 deletions mappings/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

glob "github.com/ryanuber/go-glob"
log "github.com/sirupsen/logrus"
"k8s.io/client-go/pkg/api/v1"
v1 "k8s.io/client-go/pkg/api/v1"

"github.com/jtblin/kube2iam"
"github.com/jtblin/kube2iam/iam"
Expand Down Expand Up @@ -100,7 +100,7 @@ func (r *RoleMapper) checkRoleForNamespace(roleArn string, namespace string) boo

ns, err := r.store.NamespaceByName(namespace)
if err != nil {
log.Debug("Unable to find an indexed namespace of %s", namespace)
log.Debugf("Unable to find an indexed namespace of %s", namespace)
return false
}

Expand Down Expand Up @@ -163,13 +163,13 @@ func (r *RoleMapper) DumpDebugInfo() map[string]interface{} {
// NewRoleMapper returns a new RoleMapper for use.
func NewRoleMapper(roleKey string, externalIDKey string, defaultRole string, namespaceRestriction bool, namespaceKey string, iamInstance *iam.Client, kubeStore store, namespaceRestrictionFormat string) *RoleMapper {
return &RoleMapper{
defaultRoleARN: iamInstance.RoleARN(defaultRole),
iamRoleKey: roleKey,
iamExternalIDKey: externalIDKey,
namespaceKey: namespaceKey,
namespaceRestriction: namespaceRestriction,
iam: iamInstance,
store: kubeStore,
defaultRoleARN: iamInstance.RoleARN(defaultRole),
iamRoleKey: roleKey,
iamExternalIDKey: externalIDKey,
namespaceKey: namespaceKey,
namespaceRestriction: namespaceRestriction,
iam: iamInstance,
store: kubeStore,
namespaceRestrictionFormat: namespaceRestrictionFormat,
}
}
76 changes: 61 additions & 15 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ const (
// Keeps track of the names of registered handlers for metric value/label initialization
var registeredHandlerNames []string

// Allowed user-agent prefixes when --metadata-protection is enabled
var userAgentPrefixAllowlist []string

// Server encapsulates all of the parameters necessary for starting up
// the server. These can either be set via command line or directly.
type Server struct {
Expand All @@ -65,6 +68,7 @@ type Server struct {
NamespaceRestrictionFormat string
UseRegionalStsEndpoint bool
AddIPTablesRule bool
MetadataProtection bool
AutoDiscoverBaseArn bool
AutoDiscoverDefaultRole bool
Debug bool
Expand All @@ -80,6 +84,7 @@ type Server struct {
InstanceID string
HealthcheckFailReason string
healthcheckTicker *time.Ticker
server *http.Server
}

type appHandlerFunc func(*log.Entry, http.ResponseWriter, *http.Request)
Expand All @@ -94,6 +99,10 @@ type responseWriter struct {
statusCode int
}

func init() {
userAgentPrefixAllowlist = []string{"aws-sdk-", "Botocore/", "Boto3/", "aws-cli/", "aws-chalice/"}
}

func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
Expand Down Expand Up @@ -365,8 +374,20 @@ func write(logger *log.Entry, w http.ResponseWriter, s string) {
}
}

// Run runs the specified Server.
func (s *Server) Run(host, token, nodeName string, insecure bool) error {
func (s *Server) checkUserAgent(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userAgent := r.UserAgent()
for _, prefix := range userAgentPrefixAllowlist {
if strings.HasPrefix(userAgent, prefix) {
next.ServeHTTP(w, r)
return
}
}
http.Error(w, fmt.Sprintf("User-agent '%s' is not allowed", userAgent), http.StatusForbidden)
})
}

func (s *Server) setup(host, token, nodeName string, insecure bool) error {
k, err := k8s.NewClient(host, token, nodeName, insecure)
if err != nil {
return err
Expand All @@ -393,17 +414,7 @@ func (s *Server) Run(host, token, nodeName string, insecure bool) error {
s.beginPollHealthcheck(healthcheckInterval)

r := mux.NewRouter()
securityHandler := newAppHandler("securityCredentialsHandler", s.securityCredentialsHandler)

if s.Debug {
// This is a potential security risk if enabled in some clusters, hence the flag
r.Handle("/debug/store", newAppHandler("debugStoreHandler", s.debugStoreHandler))
}
r.Handle("/{version}/meta-data/iam/security-credentials", securityHandler)
r.Handle("/{version}/meta-data/iam/security-credentials/", securityHandler)
r.Handle(
"/{version}/meta-data/iam/security-credentials/{role:.*}",
newAppHandler("roleHandler", s.roleHandler))
r.Handle("/healthz", newAppHandler("healthHandler", s.healthHandler))

if s.MetricsPort == s.AppPort {
Expand All @@ -412,13 +423,48 @@ func (s *Server) Run(host, token, nodeName string, insecure bool) error {
metrics.StartMetricsServer(s.MetricsPort)
}

if s.Debug {
// This is a potential security risk if enabled in some clusters, hence the flag
r.Handle("/debug/store", newAppHandler("debugStoreHandler", s.debugStoreHandler))
}

sr := r.NewRoute().Subrouter()
if s.MetadataProtection {
// All routes added to this subrouter will have user-agent validation
sr.Use(s.checkUserAgent)
}

securityHandler := newAppHandler("securityCredentialsHandler", s.securityCredentialsHandler)

sr.Handle("/{version}/meta-data/iam/security-credentials", securityHandler)
sr.Handle("/{version}/meta-data/iam/security-credentials/", securityHandler)
sr.Handle(
"/{version}/meta-data/iam/security-credentials/{role:.*}",
newAppHandler("roleHandler", s.roleHandler))

// This has to be registered last so that it catches fall-throughs
r.Handle("/{path:.*}", newAppHandler("reverseProxyHandler", s.reverseProxyHandler))
sr.Handle("/{path:.*}", newAppHandler("reverseProxyHandler", s.reverseProxyHandler))

log.Infof("Listening on port %s", s.AppPort)
if err := http.ListenAndServe(":"+s.AppPort, r); err != nil {
srv := http.Server{
Addr: ":" + s.AppPort,
Handler: r,
}
s.server = &srv

return nil
}

// Run runs the specified Server.
func (s *Server) Run(host, token, nodeName string, insecure bool) error {
err := s.setup(host, token, nodeName, insecure)
if err != nil {
log.Fatalf("Error creating kube2iam http server: %+v", err)
}
log.Infof("Listening on port %s", s.AppPort)
if err = s.server.ListenAndServe(); err != nil {
log.Fatalf("Error starting http server: %+v", err)
}

return nil
}

Expand Down
168 changes: 168 additions & 0 deletions server/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package server

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"testing"

"k8s.io/client-go/pkg/api/unversioned"

v1 "k8s.io/client-go/pkg/api/v1"
)

func withServer(t *testing.T, metadataProtection bool, request *http.Request, callback func(rr *httptest.ResponseRecorder)) {
kubernetesService := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
if r.URL.Path == "/api/v1/namespaces" {
responseJSON, err := json.Marshal(v1.NamespaceList{
TypeMeta: unversioned.TypeMeta{
Kind: "NamespaceList",
APIVersion: "v1",
},
Items: []v1.Namespace{},
})
if err != nil {
t.Errorf("Failed to marshal json: %v", err)
}

_, err = rw.Write(responseJSON)
if err != nil {
t.Errorf("Failed to write response: %v", err)
}
} else if r.URL.Path == "/api/v1/pods" {
responseJSON, err := json.Marshal(v1.PodList{
TypeMeta: unversioned.TypeMeta{
Kind: "PodList",
APIVersion: "v1",
},
Items: []v1.Pod{
{
Status: v1.PodStatus{
PodIP: "10.0.0.1",
},
ObjectMeta: v1.ObjectMeta{
Annotations: map[string]string{
"iam.amazonaws.com/role": "my-role",
},
},
},
},
})
if err != nil {
t.Errorf("Failed to marshal json: %v", err)
}

_, err = rw.Write(responseJSON)
if err != nil {
t.Errorf("Failed to write response: %v", err)
}
}
}))
defer kubernetesService.Close()

metadataService := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/latest/meta-data/instance-id" {
rw.Write([]byte("instanceid"))
}
}))
defer metadataService.Close()

server := NewServer()
metadataURL, err := url.Parse(metadataService.URL)
if err != nil {
t.Errorf("Failed to parse metadata url: %v", err)
}
server.MetadataAddress = metadataURL.Host
server.MetadataProtection = metadataProtection

err = server.setup(kubernetesService.URL, "-", "", true)
if err != nil {
t.Errorf("Error starting server: %v", err)
}

rr := httptest.NewRecorder()
request.RemoteAddr = "10.0.0.1:80"
server.server.Handler.ServeHTTP(rr, request)
callback(rr)
}

func TestMetadataProtection(t *testing.T) {
t.Run("allows requests with any user-agent when metadata protection is disabled", func(tt *testing.T) {
request, err := http.NewRequest("GET", "http://127.0.0.1/latest/meta-data/iam/security-credentials", nil)
if err != nil {
fmt.Printf("Failed to make request: %v", err)
}

withServer(t, false, request, func(rr *httptest.ResponseRecorder) {
if rr.Code != 200 {
t.Errorf("Expected 200, got %d", rr.Code)
}

body, err := ioutil.ReadAll(rr.Body)
if err != nil {
t.Errorf("Error reading body: %v", err)
}
if string(body) != "my-role" {
t.Errorf("Got unexpected role")
}
})
})

t.Run("blocks requests with wrong user-agent when metadata protection is enabled", func(tt *testing.T) {
request, err := http.NewRequest("GET", "http://127.0.0.1/latest/meta-data/iam/security-credentials", nil)
if err != nil {
fmt.Printf("Failed to make request: %v", err)
}

withServer(t, true, request, func(rr *httptest.ResponseRecorder) {
if rr.Code != 403 {
t.Errorf("Expected 403, got %d", rr.Code)
}
})
})

t.Run("allows requests with the correct user-agent when metadata protection is enabled", func(tt *testing.T) {
request, err := http.NewRequest("GET", "http://127.0.0.1/latest/meta-data/iam/security-credentials", nil)
request.Header.Set("User-Agent", "aws-cli/1.0")
if err != nil {
fmt.Printf("Failed to make request: %v", err)
}

withServer(t, true, request, func(rr *httptest.ResponseRecorder) {
if rr.Code != 200 {
t.Errorf("Expected 200, got %d", rr.Code)
}

body, err := ioutil.ReadAll(rr.Body)
if err != nil {
t.Errorf("Error reading body: %v", err)
}
if string(body) != "my-role" {
t.Errorf("Got unexpected role")
}
})
})

t.Run("allows healthchecks with wrong user-agent when metadata protection is enabled", func(tt *testing.T) {
request, err := http.NewRequest("GET", "http://127.0.0.1/healthz", nil)
if err != nil {
fmt.Printf("Failed to make request: %v", err)
}

withServer(t, true, request, func(rr *httptest.ResponseRecorder) {
if rr.Code != 200 {
t.Errorf("Expected 200, got %d", rr.Code)
}

_, err := ioutil.ReadAll(rr.Body)
if err != nil {
t.Errorf("Error reading body: %v", err)
}
})
})

}

0 comments on commit bd4ab8e

Please sign in to comment.