Skip to content

Commit

Permalink
s3proxy: add intial implementation
Browse files Browse the repository at this point in the history
INSECURE!
The proxy intercepts GetObject and PutObject.
A manual deployment guide is included.
The decryption only relies on a hardcoded, static key.
Do not use with sensitive data; testing only.
* Ticket to track ranged GetObject: AB#3466.
  • Loading branch information
derpsteb committed Oct 6, 2023
1 parent 85b4101 commit 59287a2
Show file tree
Hide file tree
Showing 13 changed files with 1,233 additions and 0 deletions.
8 changes: 8 additions & 0 deletions bazel/oci/containers.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ def containers():
"repotag_file": "//bazel/release:libvirt_tag.txt",
"used_by": ["config"],
},
{
"identifier": "s3proxy",
"image_name": "s3proxy",
"name": "s3proxy",
"oci": "//s3proxy/cmd:s3proxy",
"repotag_file": "//bazel/release:s3proxy_tag.txt",
"used_by": ["config"],
},
]

def helm_containers():
Expand Down
47 changes: 47 additions & 0 deletions s3proxy/cmd/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_cross_binary", "go_library")
load("@rules_oci//oci:defs.bzl", "oci_image")
load("@rules_pkg//:pkg.bzl", "pkg_tar")

go_library(
name = "cmd_lib",
srcs = ["main.go"],
importpath = "github.com/edgelesssys/constellation/v2/s3proxy/cmd",
visibility = ["//visibility:private"],
deps = [
"//internal/logger",
"//s3proxy/internal/router",
"@org_uber_go_zap//:zap",
],
)

go_binary(
name = "cmd",
embed = [":cmd_lib"],
visibility = ["//visibility:public"],
)

go_cross_binary(
name = "s3proxy_linux_amd64",
platform = "@io_bazel_rules_go//go/toolchain:linux_amd64",
target = ":cmd",
visibility = ["//visibility:public"],
)

pkg_tar(
name = "layer",
srcs = [
":s3proxy_linux_amd64",
],
mode = "0755",
remap_paths = {"/s3proxy_linux_amd64": "/s3proxy"},
)

oci_image(
name = "s3proxy",
base = "@distroless_static_linux_amd64",
entrypoint = ["/s3proxy"],
tars = [
":layer",
],
visibility = ["//visibility:public"],
)
121 changes: 121 additions & 0 deletions s3proxy/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/

/*
Package main parses command line flags and starts the s3proxy server.
*/
package main

import (
"crypto/tls"
"flag"
"fmt"
"net"
"net/http"

"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/s3proxy/internal/router"
"go.uber.org/zap"
)

const (
// defaultPort is the default port to listen on.
defaultPort = 4433
// defaultIP is the default IP to listen on.
defaultIP = "0.0.0.0"
// defaultRegion is the default AWS region to use.
defaultRegion = "eu-west-1"
// defaultCertLocation is the default location of the TLS certificate.
defaultCertLocation = "/etc/s3proxy/certs"
// defaultLogLevel is the default log level.
defaultLogLevel = 0
)

func main() {
flags, err := parseFlags()
if err != nil {
panic(err)
}

// logLevel can be made a public variable so logging level can be changed dynamically.
// TODO (derpsteb): enable once we are on go 1.21.
// logLevel := new(slog.LevelVar)
// handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel})
// logger := slog.New(handler)
// logLevel.Set(flags.logLevel)

logger := logger.New(logger.JSONLog, logger.VerbosityFromInt(flags.logLevel))

if err := runServer(flags, logger); err != nil {
panic(err)
}
}

func runServer(flags cmdFlags, log *logger.Logger) error {
log.With(zap.String("ip", flags.ip), zap.Int("port", defaultPort), zap.String("region", flags.region)).Infof("listening")

router := router.New(flags.region, log)

server := http.Server{
Addr: fmt.Sprintf("%s:%d", flags.ip, defaultPort),
Handler: http.HandlerFunc(router.Serve),
// Disable HTTP/2. Serving HTTP/2 will cause some clients to use HTTP/2.
// It seems like AWS S3 does not support HTTP/2.
// Having HTTP/2 enabled will at least cause the aws-sdk-go V1 copy-object operation to fail.
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){},
}

// i.e. if TLS is enabled.
if !flags.noTLS {
cert, err := tls.LoadX509KeyPair(flags.certLocation+"/s3proxy.crt", flags.certLocation+"/s3proxy.key")
if err != nil {
return fmt.Errorf("loading TLS certificate: %w", err)
}

server.TLSConfig = &tls.Config{
Certificates: []tls.Certificate{cert},
}

// TLSConfig is populated, so we can safely pass empty strings to ListenAndServeTLS.
return server.ListenAndServeTLS("", "")
}

log.Warnf("TLS is disabled")
return server.ListenAndServe()
}

func parseFlags() (cmdFlags, error) {
noTLS := flag.Bool("no-tls", false, "disable TLS and listen on port 80, otherwise listen on 443")
ip := flag.String("ip", defaultIP, "ip to listen on")
region := flag.String("region", defaultRegion, "AWS region in which target bucket is located")
certLocation := flag.String("cert", defaultCertLocation, "location of TLS certificate")
level := flag.Int("level", defaultLogLevel, "log level")

flag.Parse()

netIP := net.ParseIP(*ip)
if netIP == nil {
return cmdFlags{}, fmt.Errorf("not a valid IPv4 address: %s", *ip)
}

// TODO(derpsteb): enable once we are on go 1.21.
// logLevel := new(slog.Level)
// if err := logLevel.UnmarshalText([]byte(*level)); err != nil {
// return cmdFlags{}, fmt.Errorf("parsing log level: %w", err)
// }

return cmdFlags{noTLS: *noTLS, ip: netIP.String(), region: *region, certLocation: *certLocation, logLevel: *level}, nil
}

type cmdFlags struct {
noTLS bool
ip string
region string
certLocation string
// TODO(derpsteb): enable once we are on go 1.21.
// logLevel slog.Level
logLevel int
}
60 changes: 60 additions & 0 deletions s3proxy/deploy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Deploying s3proxy

Disclaimer: the following steps will be automated next.
- Within `constellation/build`: `bazel run //:devbuild`
- Copy the container name displayed for the s3proxy image. Look for the line starting with `[@//bazel/release:s3proxy_push]`.
- Replace the image key in `deployment-s3proxy.yaml` with the image value you just copied. Use the sha256 hash instead of the tag to make sure you use the latest image.
- Replace the `replaceme` values with valid AWS credentials. The s3proxy uses those credentials to access S3.
- Run `kubectl apply -f deployment-s3proxy.yaml`

# Deploying Filestash

Filestash is a demo application that can be used to see s3proxy in action.
To deploy Filestash, first deploy s3proxy as described above.
Then run the below commands:

```sh
$ cat << EOF > "deployment-filestash.yaml"
apiVersion: apps/v1
kind: Deployment
metadata:
name: filestash
spec:
replicas: 1
selector:
matchLabels:
app: filestash
template:
metadata:
labels:
app: filestash
spec:
imagePullSecrets:
- name: regcred
hostAliases:
- ip: $(kubectl get svc s3proxy-service -o=jsonpath='{.spec.clusterIP}')
hostnames:
- "s3.eu-west-1.amazonaws.com"
containers:
- name: filestash
image: machines/filestash:latest
ports:
- containerPort: 8334
volumeMounts:
- name: ca-cert
mountPath: /etc/ssl/certs/kube-ca.crt
subPath: kube-ca.crt
volumes:
- name: ca-cert
secret:
secretName: s3proxy-tls
items:
- key: ca.crt
path: kube-ca.crt
EOF

$ kubectl apply -f deployment-filestash.yaml
```

Afterwards you can use a port forward to access the Filestash pod:
- `kubectl port-forward pod/$(kubectl get pod --selector='app=filestash' -o=jsonpath='{.items[*].metadata.name}') 8443:8443`
94 changes: 94 additions & 0 deletions s3proxy/deploy/deployment-s3proxy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: selfsigned-issuer
labels:
app: s3proxy
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: selfsigned-ca
labels:
app: s3proxy
spec:
isCA: true
commonName: s3proxy-selfsigned-ca
secretName: s3proxy-tls
privateKey:
algorithm: ECDSA
size: 256
dnsNames:
- "s3.eu-west-1.amazonaws.com"
issuerRef:
name: selfsigned-issuer
kind: ClusterIssuer
group: cert-manager.io
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: s3proxy
labels:
app: s3proxy
spec:
replicas: 1
selector:
matchLabels:
app: s3proxy
template:
metadata:
labels:
app: s3proxy
spec:
imagePullSecrets:
- name: regcred
containers:
- name: s3proxy
image: ghcr.io/edgelesssys/constellation/s3proxy@sha256:2394a804e8b5ff487a55199dd83138885322a4de8e71ac7ce67b79d4ffc842b2
args:
- "--level=-1"
ports:
- containerPort: 4433
name: s3proxy-port
volumeMounts:
- name: tls-cert-data
mountPath: /etc/s3proxy/certs/s3proxy.crt
subPath: tls.crt
- name: tls-cert-data
mountPath: /etc/s3proxy/certs/s3proxy.key
subPath: tls.key
envFrom:
- secretRef:
name: s3-creds
volumes:
- name: tls-cert-data
secret:
secretName: s3proxy-tls
- name: s3-creds
secret:
secretName: s3-creds
---
apiVersion: v1
kind: Service
metadata:
name: s3proxy-service
spec:
selector:
app: s3proxy
ports:
- name: https
port: 443
targetPort: s3proxy-port
type: ClusterIP
---
apiVersion: v1
kind: Secret
metadata:
name: s3-creds
type: Opaque
stringData:
AWS_ACCESS_KEY_ID: "replaceme"
AWS_SECRET_ACCESS_KEY: "replaceme"
8 changes: 8 additions & 0 deletions s3proxy/internal/crypto/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")

go_library(
name = "crypto",
srcs = ["crypto.go"],
importpath = "github.com/edgelesssys/constellation/v2/s3proxy/internal/crypto",
visibility = ["//s3proxy:__subpackages__"],
)
Loading

0 comments on commit 59287a2

Please sign in to comment.