Skip to content

Commit

Permalink
update docs; godocs; goreleaser; images; add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
salrashid123 committed Mar 11, 2024
1 parent a4b03b7 commit 84d8cbb
Show file tree
Hide file tree
Showing 12 changed files with 452 additions and 165 deletions.
1 change: 1 addition & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ scoop:
goamd64: v1
builds:
- id: gce_metadata_server
main: ./cmd
goos:
- linux
- darwin
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ FROM docker.io/golang@sha256:6fbd2d3398db924f8d708cf6e94bd3a436bb468195daa6a96e8
WORKDIR /go/src/app
COPY . .
RUN go mod download
RUN GOOS=linux GOARCH=amd64 go build cmd/main.go -buildvcs=false -o /go/bin/gce_metadata_server
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -buildvcs=false -o /go/bin/gce_metadata_server cmd/main.go
RUN chown root:root /go/bin/gce_metadata_server

# base-debian11-root
FROM gcr.io/distroless/base-debian11@sha256:b31a6e02605827e77b7ebb82a0ac9669ec51091edd62c2c076175e05556f4ab9
FROM gcr.io/distroless/base@sha256:b31a6e02605827e77b7ebb82a0ac9669ec51091edd62c2c076175e05556f4ab9
COPY --from=build /go/bin/gce_metadata_server /gce_metadata_server
EXPOSE 8080
ENTRYPOINT [ "/gce_metadata_server" ]
322 changes: 200 additions & 122 deletions README.md

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion cmd/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ go_library(
srcs = [
"main.go",
],
importpath = "github.com/salrashid123/gce_metadata_server/cmd",
visibility = ["//visibility:private"],
deps = [
"//:go_default_library",
Expand Down
53 changes: 35 additions & 18 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ func main() {
os.Exit(-1)
}

var claims mds.Claims
err = json.Unmarshal(configData, &claims)
claims := &mds.Claims{}
err = json.Unmarshal(configData, claims)
if err != nil {
glog.Errorf("Error parsing json: %v\n", err)
os.Exit(-1)
Expand Down Expand Up @@ -90,7 +90,7 @@ func main() {
glog.Infoln("Using TPM based token handle")

if *persistentHandle == 0 {
glog.Error("persistent handle must be specified TPM")
glog.Error("persistent handle must be specified")
os.Exit(1)
}
// verify we actually have access to the TPM
Expand All @@ -101,14 +101,13 @@ func main() {
}
err = rwc.Close()
if err != nil {
glog.Errorf("can't closing TPM %s: %v", *tpmPath, err)
glog.Errorf("can't close TPM %s: %v", *tpmPath, err)
os.Exit(1)
}
} else {

glog.Infoln("Using serviceAccountFile for credentials")
var err error
//creds, err = google.FindDefaultCredentials(ctx, tokenScopes)
data, err := os.ReadFile(*serviceAccountFile)
if err != nil {
glog.Errorf("Unable to read serviceAccountFile %v", err)
Expand All @@ -121,22 +120,40 @@ func main() {
}

if creds.ProjectID != claims.ComputeMetadata.V1.Project.ProjectID {
glog.Warning("ProjectID in config file does not match project from credentials")
glog.Warningf("Warning: ProjectID in config file [%s] does not match project from credentials [%s]", claims.ComputeMetadata.V1.Project.ProjectID, creds.ProjectID)
}

// compare the svc account email in the cred file vs the config file
// note json struct for the service account file isn't exported https://github.com/golang/oauth2/blob/master/google/google.go#L109
// for now i'm parsing it directly
credJsonMap := make(map[string](interface{}))
err = json.Unmarshal(creds.JSON, &credJsonMap)
if err != nil {
glog.Errorf("Unable to parse serviceAccountFile as json %v ", err)
os.Exit(1)
}
credFileEmail := credJsonMap["client_email"]
if credFileEmail != claims.ComputeMetadata.V1.Instance.ServiceAccounts["default"].Email {
glog.Warningf("Warning: service account email in config file [%s] does not match project from credentials [%s]", claims.ComputeMetadata.V1.Instance.ServiceAccounts["default"].Email, credFileEmail)
}

}

serverConfig := &mds.ServerConfig{
BindInterface: *bindInterface,
Port: *port,
Impersonate: *useImpersonate,
Federate: *useFederate,
DomainSocket: *useDomainSocket,
UseTPM: *useTPM,
TPMPath: *tpmPath,
PersistentHandle: *persistentHandle,
}

f := &mds.MetadataServer{
Claims: claims,
Creds: creds,
ServerConfig: mds.ServerConfig{
BindInterface: *bindInterface,
Port: *port,
Impersonate: *useImpersonate,
Federate: *useFederate,
DomainSocket: *useDomainSocket,
TPMPath: *tpmPath,
PersistentHandle: *persistentHandle,
},
f, err := mds.NewMetadataServer(ctx, serverConfig, creds, claims)
if err != nil {
glog.Errorf("Error creating metadata server %v\n", err)
os.Exit(1)
}

done := make(chan os.Signal, 1)
Expand Down
6 changes: 3 additions & 3 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@
"preemptible": "FALSE"
},
"serviceAccounts": {
"708288290784-compute@developer.gserviceaccount.com": {
"metadata-sa@$PROJECT.iam.gserviceaccount.com": {
"aliases": [
"default"
],
"email": "708288290784-compute@developer.gserviceaccount.com",
"email": "metadata-sa@$PROJECT.iam.gserviceaccount.com",
"scopes": [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email"
Expand All @@ -81,7 +81,7 @@
"aliases": [
"default"
],
"email": "tpm-sa@core-eso.iam.gserviceaccount.com",
"email": "metadata-sa@$PROJECT.iam.gserviceaccount.com",
"scopes": [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email"
Expand Down
18 changes: 17 additions & 1 deletion examples/kubernetes/metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ spec:
spec:
containers:
- name: service
image: docker.io/salrashid123/gcemetadataserver@sha256:0c5e5b2f5172c0da2c01a73014dc325aaefb92ec825e5f9b4f383d86f11f1f07
image: docker.io/salrashid123/gcemetadataserver
args: [
"-serviceAccountFile=/certs/metadata-sa.json",
"-configFile=/config/config.json",
"-logtostderr","-alsologtostderr",
"-interface=0.0.0.0",
"-v=50",
"-port=:8080"
]
Expand All @@ -54,8 +56,22 @@ spec:
- name: metadata-sa
mountPath: "/certs"
readOnly: true
- name: mds-config-vol
mountPath: "/config"
readOnly: true
volumes:
- name: metadata-sa
secret:
secretName: gcp-svc-account
optional: false
- name: mds-config-vol
configMap:
name: mds-config
---
apiVersion: v1
kind: ConfigMap
metadata:
name: mds-config
data:
config.json: |
"replace with contents of config.json"
Binary file modified images/metadata_proxy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/setup_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed images/setup_5.png
Binary file not shown.
76 changes: 60 additions & 16 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,20 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Creates a Google Cloud Platform local MetadataServer used for test, emulation or to run
// applications outside of a google cloud environment.
//
// Supports reading credentials from a service account key file, workload federation,
// statically or from a key saved on a Trusted Platform Module (TPM).
//
// [GCE Metadata Server Emulator]: https://github.com/salrashid123/gce_metadata_server
package mds

import (
"bytes"
"encoding/json"
"errors"
"io"
"net"
"reflect"
Expand Down Expand Up @@ -52,17 +61,22 @@ import (
iamcredentialspb "cloud.google.com/go/iam/credentials/apiv1/credentialspb"
)

// Configures and manages the server and is used as a receiver to start and stop the server.
//
// Applications can initialize the metadata server using this struct by providing it
// with the credentials to use, the claims it provides as well as runtime specifications
// like the port and interface to use
type MetadataServer struct {
tokenMutex sync.Mutex
srv *http.Server
Creds *google.Credentials
Claims Claims
ServerConfig ServerConfig
initNew bool
Creds *google.Credentials // credentials to use
Claims Claims // values for the runtime attributes and values the metadata server returns
ServerConfig ServerConfig // base system configuration (listen interface, port, etc)
}

var (
hostHeaders = []string{"metadata", "metadata.google.internal", "169.254.169.254"}
quit = make(chan bool)
// hostHeaders = []string{"metadata", "metadata.google.internal", "169.254.169.254"}
)

const (
Expand All @@ -89,17 +103,19 @@ const (
`
)

// Configures the base runtime for the metadata server.
// Set the port, bind-address and what mode this server will acquire credentials through
type ServerConfig struct {
BindInterface string
Port string
DomainSocket string
BindInterface string // interface to bind to (default 127.0.0.1)
Port string // port to listen on (default :8080)
DomainSocket string // toggle if unix domain sockets should be used.

Impersonate bool
Federate bool
Impersonate bool // toggle if provided default credentials should be impersonated (default: false)
Federate bool // toggle if workload federation should be used (default: false)

UseTPM bool
TPMPath string
PersistentHandle int
UseTPM bool // toggle if TPM should be used for credentials (default: false)
TPMPath string // path to the TPM (default /dev/tpm0)
PersistentHandle int // persistent handle for the TPM pointing to the credentials (default: 0)
}

func httpError(w http.ResponseWriter, error string, code int, contentType string) {
Expand All @@ -111,11 +127,11 @@ func httpError(w http.ResponseWriter, error string, code int, contentType string
fmt.Fprintln(w, error)
}

// metadata server returns an "expires_in" while oauth2.Token returns Expiry time.time
type metadataToken struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
// metadata server returns an "expires_in" while oauth2.Token returns Expiry time.time
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
}

type serviceAccountDetails struct {
Expand All @@ -126,12 +142,19 @@ type serviceAccountDetails struct {
Token string `json:"token" altjson:"token"`
}

// Base claims returned by the metadata server
// Claims are structured in the same format as provided by a 'real' metadata server
//
// eg `curl -v -H 'Metadata-Flavor: Google' http://metadata/computeMetadata/v1/?recursive=true`
type Claims struct {
ComputeMetadata ComputeMetadata `json:"computeMetadata" altjson:"computeMetadata"`
}

type ComputeMetadata struct {
V1 V1 `json:"v1" altjson:"v1"`
}

// Configuration of the v1 settings for the metadata server.
type V1 struct {
Instance Instance `json:"instance" altjson:"instance"`
Oslogin OSlogin `json:"oslogin" altjson:"oslogin"`
Expand Down Expand Up @@ -193,13 +216,15 @@ type Instance struct {
Zone string `json:"zone" altjson:"zone"`
}

// OSLogin configuration to apply
type OSlogin struct {
Authenticate struct {
Sessions struct {
} `json:"sessions" altjson:"sessions"`
} `json:"authenticate" altjson:"authenticate"`
}

// Project configuration to apply
type Project struct {
Attributes map[string]string `json:"attributes" altjson:"attributes"`
NumericProjectID int64 `json:"numericProjectId" altjson:"numeric-project-id"`
Expand Down Expand Up @@ -999,8 +1024,13 @@ func (h *MetadataServer) computeMetadatav1InstanceNetworkInterfaceAccessConfigsK

}

// Start running the metadata server using the configuration provided through `NewMetadataServer()`
func (h *MetadataServer) Start() error {

if !h.initNew {
return errors.New("metadata server was not created using NewMetadataServer()")
}

r := mux.NewRouter()
r.StrictSlash(false)

Expand Down Expand Up @@ -1080,6 +1110,7 @@ func (h *MetadataServer) Start() error {
return nil
}

// Stop a running metadata server
func (h *MetadataServer) Shutdown() error {
ctx := context.Background()
if err := h.srv.Shutdown(ctx); err != nil {
Expand All @@ -1090,14 +1121,27 @@ func (h *MetadataServer) Shutdown() error {
return nil
}

// Configure a new MetadataServer instance.
//
// This will not start the instance (to do that, use the .Start() method).
//
// - ServerConfig: This configures the core/baseline runtime. Specify the interface,port and credential scheme to use
//
// - google.Credentials: Credentials to use for the access or id_token
//
// - Claims: The runtime claims returned by the metadata server
func NewMetadataServer(ctx context.Context, serverConfig *ServerConfig, creds *google.Credentials, claims *Claims) (*MetadataServer, error) {

// do some input validation here
if serverConfig == nil || creds == nil || claims == nil {
return nil, errors.New("serverConfig, credential and claims cannot be nil")
}

h := &MetadataServer{
Creds: creds,
Claims: *claims,
ServerConfig: *serverConfig,
initNew: true, // confirms the MetadataServer was started with NewMetadataServer()
}
return h, nil
}
Loading

0 comments on commit 84d8cbb

Please sign in to comment.