Skip to content

Commit

Permalink
s3proxy: add intial implementation
Browse files Browse the repository at this point in the history
The proxy intercepts GetObject and PutObject.
It does not encrypt/decrypt data.
A manual deployment guide is included.
  • Loading branch information
derpsteb committed Sep 27, 2023
1 parent f3f4944 commit cc9e07d
Show file tree
Hide file tree
Showing 12 changed files with 1,149 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
46 changes: 46 additions & 0 deletions s3proxy/cmd/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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",
],
)

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"],
)
110 changes: 110 additions & 0 deletions s3proxy/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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"
)

const (
// defaultPort is the default port to listen on.
defaultPort = 443
// defaultIP is the default IP to listen on.
defaultIP = "172.18.0.1"
// 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.Infof("listening", "ip", flags.ip, "port", flags.port, "region", flags.region)

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

server := http.Server{
Addr: fmt.Sprintf("%s:%d", flags.ip, flags.port),
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){},
}

if flags.port == 443 {
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) {
port := flag.Int("port", defaultPort, "port to listen on")
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{port: *port, ip: netIP.String(), region: *region, certLocation: *certLocation, logLevel: *level}, nil
}

type cmdFlags struct {
port int
ip string
region string
certLocation string
// TODO(derpsteb): enable once we are on go 1.21.
// logLevel slog.Level
logLevel int
}
61 changes: 61 additions & 0 deletions s3proxy/deploy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 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.
- Run the script `create_cert.sh`. This will create a certificate signed by the Kubernetes CA and store it in the cluster, including the private key. The s3proxy uses that certificate to serve HTTPS.
- 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: kube-ca
mountPath: /etc/ssl/certs/kube-ca.crt
subPath: kube-ca.crt
volumes:
- name: kube-ca
configMap:
name: kube-root-ca.crt
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`
78 changes: 78 additions & 0 deletions s3proxy/deploy/create_cert.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# SERVICE is the name of the s3proxy service in kubernetes.
# It does not have to match the actual running service, though it may help for consistency.
export SERVICE=s3proxy

# NAMESPACE where the s3proxy service is running.
export NAMESPACE=default

# SECRET_NAME to create in the kubernetes secrets store.
export SECRET_NAME=s3proxy-tls

# TMPDIR is a temporary working directory.
export TMPDIR=$(mktemp -d)

# CSR_NAME will be the name of our certificate signing request as seen by kubernetes.
export CSR_NAME=s3proxy-csr

openssl genrsa -out ${TMPDIR}/s3proxy.key 2048

cat << EOF > ${TMPDIR}/csr.conf
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = *.${SERVICE}
DNS.2 = *.${SERVICE}.${NAMESPACE}
DNS.3 = *.${SERVICE}.${NAMESPACE}.svc
DNS.4 = *.${SERVICE}.${NAMESPACE}.svc.cluster.local
DNS.5 = *.${SERVICE}-internal
DNS.6 = *.${SERVICE}-internal.${NAMESPACE}
DNS.7 = *.${SERVICE}-internal.${NAMESPACE}.svc
DNS.8 = *.${SERVICE}-internal.${NAMESPACE}.svc.cluster.local
DNS.9 = s3.eu-west-1.amazonaws.com
IP.1 = 127.0.0.1
EOF

openssl req -new -key ${TMPDIR}/s3proxy.key \
-subj "/O=system:nodes/CN=system:node:${SERVICE}.${NAMESPACE}.svc" \
-out ${TMPDIR}/server.csr \
-config ${TMPDIR}/csr.conf

cat << EOF > ${TMPDIR}/csr.yaml
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: ${CSR_NAME}
spec:
groups:
- system:authenticated
request: $(cat ${TMPDIR}/server.csr | base64 | tr -d '\r\n')
signerName: kubernetes.io/kubelet-serving
usages:
- digital signature
- key encipherment
- server auth
EOF

kubectl create -f ${TMPDIR}/csr.yaml --dry-run=client -o yaml --save-config | kubectl apply -f -
kubectl certificate approve ${CSR_NAME}
kubectl get csr ${CSR_NAME}

serverCert=$(kubectl get csr ${CSR_NAME} -o jsonpath='{.status.certificate}')
echo "${serverCert}" | openssl base64 -d -A -out ${TMPDIR}/s3proxy.crt
kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[].cluster.certificate-authority-data}' | base64 -d > ${TMPDIR}/s3proxy.ca
kubectl create namespace ${NAMESPACE} --dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic ${SECRET_NAME} \
--namespace ${NAMESPACE} \
--from-file=s3proxy.key=${TMPDIR}/s3proxy.key \
--from-file=s3proxy.crt=${TMPDIR}/s3proxy.crt \
--from-file=s3proxy.ca=${TMPDIR}/s3proxy.ca --dry-run=client -o yaml --save-config | kubectl apply -f -

rm -rf ${TMPDIR}
60 changes: 60 additions & 0 deletions s3proxy/deploy/deployment-s3proxy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: s3proxy
spec:
replicas: 1
selector:
matchLabels:
app: s3proxy
template:
metadata:
labels:
app: s3proxy
spec:
imagePullSecrets:
- name: regcred
containers:
- name: s3proxy
image: ghcr.io/derpsteb/constellation/s3proxy@sha256:57dbcf394e1464c07f2ef5c5e7fd1a87d7477c15394faa81601901f6956c06e3
args:
- "--ip=0.0.0.0"
- "--port=443"
- "--level=debug"
ports:
- containerPort: 443
volumeMounts:
- name: tls-cert-data
mountPath: /etc/s3proxy/certs
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: 443
type: ClusterIP
---
apiVersion: v1
kind: Secret
metadata:
name: s3-creds
type: Opaque
stringData:
AWS_ACCESS_KEY_ID: "replaceme"
AWS_SECRET_ACCESS_KEY: "replaceme"
24 changes: 24 additions & 0 deletions s3proxy/internal/router/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("//bazel/go:go_test.bzl", "go_test")

go_library(
name = "router",
srcs = [
"object.go",
"router.go",
],
importpath = "github.com/edgelesssys/constellation/v2/s3proxy/internal/router",
visibility = ["//s3proxy:__subpackages__"],
deps = [
"//internal/logger",
"//s3proxy/internal/s3",
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
],
)

go_test(
name = "router_test",
srcs = ["router_test.go"],
embed = [":router"],
deps = ["@com_github_stretchr_testify//assert"],
)
Loading

0 comments on commit cc9e07d

Please sign in to comment.