Skip to content

Commit 60e8a1b

Browse files
authored
Add AWS storage backend support (#204)
* Add AWS storage backend support * Remove TODOs from `storage/aws/issuers`
1 parent d6dc31c commit 60e8a1b

File tree

8 files changed

+496
-0
lines changed

8 files changed

+496
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Each Tessera storage backend needs its own SCTFE binary.
1616
At the moment, these storage backends are supported:
1717

1818
- [GCP](./cmd/gcp/): [deployment instructions](./deployment/live/gcp/test/)
19+
- [AWS](./cmd/aws/)
1920
- more to come soon!
2021

2122
## Working on the Code

cmd/aws/Dockerfile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
FROM golang:1.24.0-alpine3.21@sha256:2d40d4fc278dad38be0777d5e2a88a2c6dee51b0b29c97a764fc6c6a11ca893c AS builder
2+
3+
ARG GOFLAGS="-trimpath -buildvcs=false -buildmode=exe"
4+
ENV GOFLAGS=$GOFLAGS
5+
6+
# Move to working directory /build
7+
WORKDIR /build
8+
9+
# Copy and download dependency using go mod
10+
COPY go.mod .
11+
COPY go.sum .
12+
RUN go mod download
13+
14+
# Copy the code into the container
15+
COPY . .
16+
17+
# Build the application
18+
RUN go build -o bin/sctfe-aws ./cmd/aws
19+
20+
# Build release image
21+
FROM alpine:3.20.2@sha256:0a4eaa0eecf5f8c050e5bba433f58c052be7587ee8af3e8b3910ef9ab5fbe9f5
22+
23+
COPY --from=builder /build/bin/sctfe-aws /bin/sctfe-aws
24+
25+
ENTRYPOINT ["/bin/sctfe-aws"]

cmd/aws/main.go

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
// Copyright 2016 Google LLC. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// The ct_server binary runs the CT personality.
16+
package main
17+
18+
import (
19+
"context"
20+
"crypto/x509"
21+
"encoding/pem"
22+
"flag"
23+
"fmt"
24+
"net/http"
25+
"os"
26+
"os/signal"
27+
"strings"
28+
"sync"
29+
"syscall"
30+
"time"
31+
32+
"github.com/go-sql-driver/mysql"
33+
"github.com/prometheus/client_golang/prometheus/promhttp"
34+
sctfe "github.com/transparency-dev/static-ct"
35+
"github.com/transparency-dev/static-ct/internal/testdata"
36+
"github.com/transparency-dev/static-ct/storage"
37+
awsSCTFE "github.com/transparency-dev/static-ct/storage/aws"
38+
"github.com/transparency-dev/static-ct/storage/bbolt"
39+
tessera "github.com/transparency-dev/trillian-tessera"
40+
awsTessera "github.com/transparency-dev/trillian-tessera/storage/aws"
41+
"golang.org/x/mod/sumdb/note"
42+
"k8s.io/klog/v2"
43+
)
44+
45+
func init() {
46+
flag.Var(&notAfterStart, "not_after_start", "Start of the range of acceptable NotAfter values, inclusive. Leaving this unset implies no lower bound to the range. RFC3339 UTC format, e.g: 2024-01-02T15:04:05Z.")
47+
flag.Var(&notAfterLimit, "not_after_limit", "Cut off point of notAfter dates - only notAfter dates strictly *before* notAfterLimit will be accepted. Leaving this unset means no upper bound on the accepted range. RFC3339 UTC format, e.g: 2024-01-02T15:04:05Z.")
48+
}
49+
50+
// Global flags that affect all log instances.
51+
var (
52+
notAfterStart timestampFlag
53+
notAfterLimit timestampFlag
54+
55+
httpEndpoint = flag.String("http_endpoint", "localhost:6962", "Endpoint for HTTP (host:port).")
56+
metricsEndpoint = flag.String("metrics_endpoint", "", "Endpoint for serving metrics; if left empty, metrics will be visible on --http_endpoint.")
57+
httpDeadline = flag.Duration("http_deadline", time.Second*10, "Deadline for HTTP requests.")
58+
maskInternalErrors = flag.Bool("mask_internal_errors", false, "Don't return error strings with Internal Server Error HTTP responses.")
59+
origin = flag.String("origin", "", "Origin of the log, for checkpoints and the monitoring prefix.")
60+
bucket = flag.String("bucket", "", "Name of the bucket to store the log in.")
61+
dbName = flag.String("db_name", "", "AuroraDB name")
62+
dbHost = flag.String("db_host", "", "AuroraDB host")
63+
dbPort = flag.Int("db_port", 3306, "AuroraDB port")
64+
dbUser = flag.String("db_user", "", "AuroraDB user")
65+
dbPassword = flag.String("db_password", "", "AuroraDB password")
66+
dbMaxConns = flag.Int("db_max_conns", 0, "Maximum connections to the database, defaults to 0, i.e unlimited")
67+
dbMaxIdle = flag.Int("db_max_idle_conns", 2, "Maximum idle database connections in the connection pool, defaults to 2")
68+
dedupPath = flag.String("dedup_path", "", "Path to the deduplication database.")
69+
rootsPemFile = flag.String("roots_pem_file", "", "Path to the file containing root certificates that are acceptable to the log. The certs are served through get-roots endpoint.")
70+
rejectExpired = flag.Bool("reject_expired", false, "If true then the certificate validity period will be checked against the current time during the validation of submissions. This will cause expired certificates to be rejected.")
71+
rejectUnexpired = flag.Bool("reject_unexpired", false, "If true then CTFE rejects certificates that are either currently valid or not yet valid.")
72+
extKeyUsages = flag.String("ext_key_usages", "", "If set, will restrict the set of such usages that the server will accept. By default all are accepted. The values specified must be ones known to the x509 package.")
73+
rejectExtensions = flag.String("reject_extension", "", "A list of X.509 extension OIDs, in dotted string form (e.g. '2.3.4.5') which, if present, should cause submissions to be rejected.")
74+
)
75+
76+
// nolint:staticcheck
77+
func main() {
78+
klog.InitFlags(nil)
79+
flag.Parse()
80+
ctx := context.Background()
81+
82+
// TODO: Replace the fake signer with AWS Secrets Manager Signer.
83+
block, _ := pem.Decode([]byte(testdata.DemoPublicKey))
84+
key, err := x509.ParsePKIXPublicKey(block.Bytes)
85+
if err != nil {
86+
klog.Exitf("Can't parse public key: %v", err)
87+
}
88+
fakeSigner := testdata.NewSignerWithFixedSig(key, []byte("sig"))
89+
90+
chainValidationConfig := sctfe.ChainValidationConfig{
91+
RootsPEMFile: *rootsPemFile,
92+
RejectExpired: *rejectExpired,
93+
RejectUnexpired: *rejectUnexpired,
94+
ExtKeyUsages: *extKeyUsages,
95+
RejectExtensions: *rejectExtensions,
96+
NotAfterStart: notAfterStart.t,
97+
NotAfterLimit: notAfterLimit.t,
98+
}
99+
100+
logHandler, err := sctfe.NewLogHandler(ctx, *origin, fakeSigner, chainValidationConfig, newAWSStorage, *httpDeadline, *maskInternalErrors)
101+
if err != nil {
102+
klog.Exitf("Can't initialize CT HTTP Server: %v", err)
103+
}
104+
105+
klog.CopyStandardLogTo("WARNING")
106+
klog.Info("**** CT HTTP Server Starting ****")
107+
http.Handle("/", logHandler)
108+
109+
metricsAt := *metricsEndpoint
110+
if metricsAt == "" {
111+
metricsAt = *httpEndpoint
112+
}
113+
114+
if metricsAt != *httpEndpoint {
115+
// Run a separate handler for metrics.
116+
go func() {
117+
mux := http.NewServeMux()
118+
mux.Handle("/metrics", promhttp.Handler())
119+
metricsServer := http.Server{Addr: metricsAt, Handler: mux}
120+
err := metricsServer.ListenAndServe()
121+
klog.Warningf("Metrics server exited: %v", err)
122+
}()
123+
} else {
124+
// Handle metrics on the DefaultServeMux.
125+
http.Handle("/metrics", promhttp.Handler())
126+
}
127+
128+
// Bring up the HTTP server and serve until we get a signal not to.
129+
srv := http.Server{Addr: *httpEndpoint}
130+
shutdownWG := new(sync.WaitGroup)
131+
go awaitSignal(func() {
132+
shutdownWG.Add(1)
133+
defer shutdownWG.Done()
134+
// Allow 60s for any pending requests to finish then terminate any stragglers
135+
// TODO(phboneff): maybe wait for the sequencer queue to be empty?
136+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*60)
137+
defer cancel()
138+
klog.Info("Shutting down HTTP server...")
139+
if err := srv.Shutdown(ctx); err != nil {
140+
klog.Errorf("srv.Shutdown(): %v", err)
141+
}
142+
klog.Info("HTTP server shutdown")
143+
})
144+
145+
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
146+
klog.Warningf("Server exited: %v", err)
147+
}
148+
// Wait will only block if the function passed to awaitSignal was called,
149+
// in which case it'll block until the HTTP server has gracefully shutdown
150+
shutdownWG.Wait()
151+
klog.Flush()
152+
}
153+
154+
// awaitSignal waits for standard termination signals, then runs the given
155+
// function; it should be run as a separate goroutine.
156+
func awaitSignal(doneFn func()) {
157+
// Arrange notification for the standard set of signals used to terminate a server
158+
sigs := make(chan os.Signal, 1)
159+
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
160+
161+
// Now block main and wait for a signal
162+
sig := <-sigs
163+
klog.Warningf("Signal received: %v", sig)
164+
klog.Flush()
165+
166+
doneFn()
167+
}
168+
169+
func newAWSStorage(ctx context.Context, signer note.Signer) (*storage.CTStorage, error) {
170+
awsCfg := storageConfigFromFlags()
171+
driver, err := awsTessera.New(ctx, awsCfg)
172+
if err != nil {
173+
return nil, fmt.Errorf("failed to initialize AWS Tessera storage driver: %v", err)
174+
}
175+
appender, _, _, err := tessera.NewAppender(ctx, driver, tessera.NewAppendOptions().
176+
WithCheckpointSigner(signer).
177+
WithCTLayout())
178+
if err != nil {
179+
return nil, fmt.Errorf("failed to initialize AWS Tessera storage: %v", err)
180+
}
181+
182+
issuerStorage, err := awsSCTFE.NewIssuerStorage(ctx, *bucket, "fingerprints/", "application/pkix-cert")
183+
if err != nil {
184+
return nil, fmt.Errorf("failed to initialize AWS issuer storage: %v", err)
185+
}
186+
187+
beDedupStorage, err := bbolt.NewStorage(*dedupPath)
188+
if err != nil {
189+
return nil, fmt.Errorf("failed to initialize BBolt deduplication database: %v", err)
190+
}
191+
192+
return storage.NewCTStorage(appender, issuerStorage, beDedupStorage)
193+
}
194+
195+
type timestampFlag struct {
196+
t *time.Time
197+
}
198+
199+
func (t *timestampFlag) String() string {
200+
if t.t != nil {
201+
return t.t.Format(time.RFC3339)
202+
}
203+
return ""
204+
}
205+
206+
func (t *timestampFlag) Set(w string) error {
207+
if !strings.HasSuffix(w, "Z") {
208+
return fmt.Errorf("timestamps MUST be in UTC, got %v", w)
209+
}
210+
tt, err := time.Parse(time.RFC3339, w)
211+
if err != nil {
212+
return fmt.Errorf("can't parse %q as RFC3339 timestamp: %v", w, err)
213+
}
214+
t.t = &tt
215+
return nil
216+
}
217+
218+
// storageConfigFromFlags returns an aws.Config struct populated with values
219+
// provided via flags.
220+
func storageConfigFromFlags() awsTessera.Config {
221+
if *bucket == "" {
222+
klog.Exit("--bucket must be set")
223+
}
224+
if *dbName == "" {
225+
klog.Exit("--db_name must be set")
226+
}
227+
if *dbHost == "" {
228+
klog.Exit("--db_host must be set")
229+
}
230+
if *dbPort == 0 {
231+
klog.Exit("--db_port must be set")
232+
}
233+
if *dbUser == "" {
234+
klog.Exit("--db_user must be set")
235+
}
236+
// Empty passord isn't an option with AuroraDB MySQL.
237+
if *dbPassword == "" {
238+
klog.Exit("--db_password must be set")
239+
}
240+
241+
c := mysql.Config{
242+
User: *dbUser,
243+
Passwd: *dbPassword,
244+
Net: "tcp",
245+
Addr: fmt.Sprintf("%s:%d", *dbHost, *dbPort),
246+
DBName: *dbName,
247+
AllowCleartextPasswords: true,
248+
AllowNativePasswords: true,
249+
}
250+
251+
return awsTessera.Config{
252+
Bucket: *bucket,
253+
DSN: c.FormatDSN(),
254+
MaxOpenConns: *dbMaxConns,
255+
MaxIdleConns: *dbMaxIdle,
256+
}
257+
}

deployment/live/aws/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# AWS live configs
2+
3+
TODO: Add terraform configuration files.
4+
5+
## Temporary Dev Env Setup
6+
7+
### Create the MySQL database and user
8+
9+
```sh
10+
mysql -h tesseract.cluster-xxxx12345678.us-east-1.rds.amazonaws.com -u admin -p
11+
```
12+
13+
```sql
14+
CREATE DATABASE tesseract;
15+
CREATE USER 'tesseract'@'%' IDENTIFIED BY 'tesseract';
16+
GRANT ALL PRIVILEGES ON tesseract.* TO 'tesseract'@'%';
17+
FLUSH PRIVILEGES;
18+
```
19+
20+
### Create the S3 bucket
21+
22+
### Start the TesseraCT server
23+
24+
```sh
25+
export AWS_REGION=us-east-1
26+
export AWS_PROFILE=AdministratorAccess-<REDACTED>
27+
```
28+
29+
```sh
30+
go run ./cmd/aws \
31+
--http_endpoint=localhost:6962 \
32+
--roots_pem_file=./internal/testdata/fake-ca.cert \
33+
--origin=test-static-ct \
34+
--bucket=test-static-ct \
35+
--db_name=tesseract \
36+
--db_host=tesseract.cluster-xxxx12345678.us-east-1.rds.amazonaws.com \
37+
--db_port=3306 \
38+
--db_user=tesseract \
39+
--db_password=tesseract \
40+
--dedup_path=test-static-ct
41+
```

go.mod

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ require (
77
cloud.google.com/go/spanner v1.77.0
88
cloud.google.com/go/storage v1.51.0
99
github.com/RobinUS2/golang-moving-average v1.0.0
10+
github.com/aws/aws-sdk-go-v2 v1.36.3
11+
github.com/aws/aws-sdk-go-v2/config v1.29.9
12+
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2
13+
github.com/aws/smithy-go v1.22.3
1014
github.com/gdamore/tcell/v2 v2.8.1
15+
github.com/go-sql-driver/mysql v1.9.0
1116
github.com/golang/mock v1.7.0-rc.1
1217
github.com/google/go-cmp v0.7.0
1318
github.com/kylelemons/godebug v1.1.0
@@ -35,11 +40,26 @@ require (
3540
cloud.google.com/go/iam v1.4.1 // indirect
3641
cloud.google.com/go/longrunning v0.6.5 // indirect
3742
cloud.google.com/go/monitoring v1.24.0 // indirect
43+
filippo.io/edwards25519 v1.1.0 // indirect
3844
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 // indirect
3945
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect
4046
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
4147
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
4248
github.com/avast/retry-go/v4 v4.6.1 // indirect
49+
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
50+
github.com/aws/aws-sdk-go-v2/credentials v1.17.62 // indirect
51+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
52+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
53+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
54+
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
55+
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
56+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
57+
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect
58+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
59+
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
60+
github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect
61+
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect
62+
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect
4363
github.com/beorn7/perks v1.0.1 // indirect
4464
github.com/cespare/xxhash/v2 v2.3.0 // indirect
4565
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect

0 commit comments

Comments
 (0)