Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package cmd
import (
"context"
"fmt"
"net/url"
"os"
"strings"

Expand All @@ -20,6 +19,7 @@ import (
"github.com/databacker/mysql-backup/pkg/config"
"github.com/databacker/mysql-backup/pkg/core"
"github.com/databacker/mysql-backup/pkg/database"
"github.com/databacker/mysql-backup/pkg/identity"
"github.com/databacker/mysql-backup/pkg/remote"
"github.com/databacker/mysql-backup/pkg/storage/credentials"
)
Expand Down Expand Up @@ -134,29 +134,46 @@ func rootCmd(execs execs) (*cobra.Command, error) {
}
cmdConfig.configuration = actualConfig

if actualConfig.Telemetry != nil && actualConfig.Telemetry.URL != nil && *actualConfig.Telemetry.URL != "" {
if actualConfig.Telemetry != nil && actualConfig.Telemetry.URL != "" {

// set up telemetry with tracing
u, err := url.Parse(*actualConfig.Telemetry.URL)
// get the full URL for telemetry endpoint, based on the base URL in the config, and the fixed telemetry traces subpath.
// Especially needed in case the endpoint already includes the path.
u, err := remote.ResolveEngineEndpoint(actualConfig.Telemetry.URL, remote.TelemetryTracesRoute)
if err != nil {
return fmt.Errorf("invalid telemetry URL: %w", err)
}
tlsConfig, err := remote.GetTLSConfig(u.Hostname(), *actualConfig.Telemetry.Certificates, *actualConfig.Telemetry.Credentials)
// get the signing identity used to sign requests (for authentication to the telemetry service) based on
// the credentials in the config.
telemetryIdentity, err := identity.New(actualConfig.Telemetry.Credentials)
if err != nil {
return fmt.Errorf("unable to set up telemetry: %w", err)
return fmt.Errorf("invalid telemetry credentials: %w", err)
}
// if any specific service certificate fingerprints were provided in the config file,
// be sure to include those when creating the http client.
pins := []string(nil)
if actualConfig.Telemetry.Certificates != nil {
pins = append(pins, (*actualConfig.Telemetry.Certificates)...)
}
// create the HTTP client that accepts the additional server certificate fingerprints,
// and signs requests using our identity.
httpClient, err := remote.NewSignedClient(pins, telemetryIdentity)
if err != nil {
return fmt.Errorf("unable to set up telemetry HTTP client: %w", err)
}
opts := []otlptracehttp.Option{
// WithEndpoint expects ONLY the host (e.g., "otelep.foo.com" or "localhost:4318")
otlptracehttp.WithEndpoint(u.Host),
otlptracehttp.WithTLSClientConfig(tlsConfig),
// configure oltptrace to use our specially set up http client
otlptracehttp.WithHTTPClient(httpClient),
}
if u.Scheme == "http" {
opts = append(opts, otlptracehttp.WithInsecure())
}
if u.Path != "" {
opts = append(opts, otlptracehttp.WithURLPath(u.Path))
}

if u.Scheme == "http" {
opts = append(opts, otlptracehttp.WithInsecure())
}
tracerExporter, err := otlptracehttp.New(ctx, opts...)
if err != nil {
return fmt.Errorf("unable to set up telemetry: %w", err)
Expand Down
17 changes: 9 additions & 8 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,22 +176,23 @@ for details of each.
* `password`: string, the password
* `logging`: string, the log level, one of: error,warning,info,debug,trace; default is info
* `telemetry`: configuration for sending telemetry data (optional)
* `url`: string, URL to telemetry service
* `certificate`: string, the certificate for the telemetry server or a CA that signed the server's TLS certificate. Not required if telemetry server does not use TLS, or if the system's certificate store already contains the server's cert or CA.
* `credentials`: string, unique token provided by the remote service as credentials, base64-encoded
* `url`: string, absolute HTTP or HTTPS service base URL. The engine appends `/engines/telemetry/traces`. A URL already ending in that route is also accepted.
* `certificates`: optional list of `sha256:` certificate fingerprints. Normal system-root and hostname verification is tried first; pins are a fallback for private deployments.
* `credentials`: versioned engine credential containing a base64-encoded 32-byte random seed, positive authentication/configuration generations, and optional retained configuration generations

#### Remote Configuration

For remote configuration, the `spec` is composed of the following:

* `url`: the URL of the remote configuration; required
* `certificate`: the certificate for the server or a CA that signed the server's TLS certificate. Not required if remote server does not use TLS, or if the system's certificate store already contains the server's cert or CA.
* `credentials`: unique token provided by the remote service as credentials, base64-encoded
* `url`: the absolute HTTP or HTTPS remote-service base URL; required. The engine appends the self-only `/engines/config` route. A URL already ending in that route is also accepted. No engine ID is configured or placed in the route: the verified HTTP signature identifies the engine.
* `certificates`: optional list of `sha256:` certificate fingerprints. Normal Web PKI and hostname verification is used when possible; matching a pin never disables hostname, validity, or server-usage checks.
* `credentials`: a `databacker-credentials/v2` object. `seed` is exactly 32 random bytes in strict padded standard base64; both generations start at 1. Authentication uses a derived Ed25519 key, while encrypted configuration uses a separately derived X25519 key.

The configuration file retrieved from a remote **always** has the same structure as any config file. It even can be
saved locally and used as a local configuration. This means it also can
reference another remote configuration, just like a local one. That can in turn reference another
and so on, ad infinitum. In practice, remote service will avoid this.
reference another remote configuration, just like a local one. The engine bounds
the traversal depth and rejects repeated remote URLs so malformed chains cannot
loop indefinitely.

### Multiple Configurations

Expand Down
31 changes: 24 additions & 7 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,27 @@ sees your unencrypted data.

The data is decrypted by `mysql-backup` locally on your machine, when you retrieve the configuration.

Your access token to the remote service, stored in your local configuration file, is a
[Curve25519 private key](https://en.wikipedia.org/wiki/Curve25519), which authenticates
you to the remote service. The remote service never sees this key, only the public key, which is used to verify your identity.

This key is then used to decrypt the configuration blob, which is used to configure `mysql-backup`.

In configuration files, the key is stored base64-encoded.
An engine credential contains one 32-byte random seed. HKDF derives
separate purpose-specific keys from it: Ed25519 signs each HTTP request, and
X25519 decrypts configuration envelopes. Neither the seed nor a derived
private key is sent to the remote service. The service stores only deterministic
public-key fingerprints and public keys.

HTTP signatures cover the exact method, authority, escaped path, and query. For
telemetry they also cover the content digest, content type, and idempotency key.
The configured remote and telemetry URLs are service base URLs. The engine
derives the self-only `/engines/config` and `/engines/telemetry/traces` routes;
the verified signature key identifies the engine, so no database-assigned
engine ID is sent in either route.
HTTPS is strongly recommended because it authenticates the server, protects
request metadata and telemetry confidentiality, and protects plaintext remote
configuration responses. Plain HTTP remains available when the deployment has
other transport protections or explicitly accepts those risks. When HTTPS is
used, a configured certificate fingerprint is only a fallback for a private or
pinned deployment; hostname, validity, and server-usage checks remain mandatory.

Encrypted configuration uses X25519, HKDF-SHA-256, and ChaCha20-Poly1305 with
authenticated envelope metadata. The engine accepts only the active or an
explicitly retained positive configuration-key generation. Rollback-state
persistence is not yet implemented, so operators must not treat the current
client as enforcing monotonic configuration versions across process restarts.
16 changes: 11 additions & 5 deletions examples/configs/remote.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@ spec:
# sha256 fingerprint of certificates for the server, or one or more of the certificates in the signing chain; unneeded if the server is using a certificate signed by a well-known CA
# this is a sample fingerprint only
# DO NOT USE THIS FINGERPRINT; GET THE ACTUAL ONE FROM YOUR REMOTE SERVER!
certificates: sha256:69729b8e15a86efc177a57afb7171dfc64add28c2fca8cf1507e34453ccb1470
# base64-encoded Curve25519 private key for authentication to the server, as well as decrypting the provided configuration
# this is a sample key only
# DO NOT USE THIS KEY; GENERATE YOUR OWN!
credentials: BwMqVfr1myxqX8tikIPYCyNtpHgMLIg/2nUE+pLQnTE=
certificates:
- sha256:69729b8e15a86efc177a57afb7171dfc64add28c2fca8cf1507e34453ccb1470
# Versioned seed credential. It derives separate Ed25519 request-signing
# and X25519 configuration-decryption keys.
# This is test data only. Generate a new 32-byte random seed for production.
credentials:
version: databacker-credentials/v2
seed: AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
authenticationGeneration: 1
configurationEncryptionGeneration: 1
retainedConfigurationEncryptionGenerations: []
14 changes: 9 additions & 5 deletions examples/configs/telemetry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ spec:
# sha256 fingerprint of certificate for the telemetry server, or one of the certificates in the signing chain; unneeded if the server is using a certificate signed by a well-known CA
# this is a sample fingerprint only
# DO NOT USE THIS FINGERPRINT; GET THE ACTUAL ONE FROM YOUR REMOTE SERVER!
certificates: sha256:69729b8e15a86efc177a57afb7171dfc64add28c2fca8cf1507e34453ccb1470
# base64-encoded Curve25519 private key for authentication to the telemetry server
# this is a sample key only
# DO NOT USE THIS KEY; GENERATE YOUR OWN!
credentials: BwMqVfr1myxqX8tikIPYCyNtpHgMLIg/2nUE+pLQnTE=
certificates:
- sha256:69729b8e15a86efc177a57afb7171dfc64add28c2fca8cf1507e34453ccb1470
# This is test data only. Generate a new 32-byte random seed for production.
credentials:
version: databacker-credentials/v2
seed: AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
authenticationGeneration: 1
configurationEncryptionGeneration: 1
retainedConfigurationEncryptionGenerations: []
# only needed if required by endpoint

# dump, or backup, configuration
Expand Down
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module github.com/databacker/mysql-backup

go 1.26.0

require github.com/databacker/api v1.7.0
require github.com/databacker/api v1.10.0

require (
github.com/aws/aws-sdk-go-v2 v1.41.6
Expand Down Expand Up @@ -33,7 +33,6 @@ require (
github.com/InfiniteLoopSpace/go_S-MIME v0.0.0-20181221134359-3f58f9a4b2b6
github.com/bramvdbogaerde/go-scp v1.5.0
github.com/gliderlabs/ssh v0.3.8
github.com/google/go-cmp v0.7.0
github.com/kevinburke/ssh_config v1.2.0
github.com/moby/go-archive v0.3.0
github.com/moby/moby/api v1.55.0
Expand Down
Loading
Loading