From d8c8aba31256c90294c7c43d018db2baf5f22374 Mon Sep 17 00:00:00 2001 From: Vitaliy Potyarkin Date: Tue, 15 Oct 2024 16:52:34 +0300 Subject: [PATCH] Add HTTP-01 solver with FrostFS backend Signed-off-by: Vitaliy Potyarkin --- cmd/flags.go | 30 +++++++++++++++ cmd/setup_challenges.go | 13 +++++++ docs/data/zz_cli_help.toml | 5 +++ providers/http/frostfs/frostfs.go | 64 +++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 providers/http/frostfs/frostfs.go diff --git a/cmd/flags.go b/cmd/flags.go index d119df84..5998071f 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -29,6 +29,11 @@ const ( flgHTTPWebroot = "http.webroot" flgHTTPMemcachedHost = "http.memcached-host" flgHTTPS3Bucket = "http.s3-bucket" + flgHTTPFrostFSEndpoint = "http.frostfs-endpoint" + flgHTTPFrostFSContainer = "http.frostfs-container" + flgHTTPFrostFSWallet = "http.frostfs-wallet" + flgHTTPFrostFSWalletAccount = "http.frostfs-wallet-account" + flgHTTPFrostFSWalletPass = "http.frostfs-wallet-password" flgTLS = "tls" flgTLSPort = "tls.port" flgDNS = "dns" @@ -135,6 +140,31 @@ func CreateFlags(defaultPath string) []cli.Flag { Name: flgHTTPS3Bucket, Usage: "Set the S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket.", }, + &cli.StringFlag{ + Name: flgHTTPFrostFSEndpoint, + Usage: "Set FrostFS endpoint to use for HTTP-01 based challenges. Challenges will be written to FrostFS container", + EnvVars: []string{"FROSTFS_ENDPOINT"}, + }, + &cli.StringFlag{ + Name: flgHTTPFrostFSContainer, + Usage: "Set FrostFS container ID to use for HTTP-01 based challenges. Challenges will be written to FrostFS container", + EnvVars: []string{"FROSTFS_CONTAINER"}, + }, + &cli.StringFlag{ + Name: flgHTTPFrostFSWallet, + Usage: "Path to NEO wallet to use for interaction with FrostFS. If no wallet is provided an ephemeral one will be generated. Such key will only work for publicly writable containers and will significantly reduce security: any attacker with knowledge of FrostFS endpoint and CID will be able to obtain certificates for your domain", + EnvVars: []string{"FROSTFS_WALLET"}, + }, + &cli.StringFlag{ + Name: flgHTTPFrostFSWalletAccount, + Usage: "Wallet account to use for interaction with FrostFS. If not set, the first account from wallet will be used", + EnvVars: []string{"FROSTFS_WALLET_ACCOUNT"}, + }, + &cli.StringFlag{ + Name: flgHTTPFrostFSWalletPass, + Usage: "Account password to decrypt the wallet. If not set, an empty password is assumed", + EnvVars: []string{"FROSTFS_WALLET_PASSWORD"}, + }, &cli.BoolFlag{ Name: flgTLS, Usage: "Use the TLS-ALPN-01 challenge to solve challenges. Can be mixed with other types of challenges.", diff --git a/cmd/setup_challenges.go b/cmd/setup_challenges.go index 2ec38198..442f0364 100644 --- a/cmd/setup_challenges.go +++ b/cmd/setup_challenges.go @@ -13,6 +13,7 @@ import ( "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/providers/dns" + "github.com/go-acme/lego/v4/providers/http/frostfs" "github.com/go-acme/lego/v4/providers/http/memcached" "github.com/go-acme/lego/v4/providers/http/s3" "github.com/go-acme/lego/v4/providers/http/webroot" @@ -67,6 +68,18 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider { log.Fatal(err) } return ps + case ctx.IsSet(flgHTTPFrostFSEndpoint): + ps, err := frostfs.NewHTTPProvider( + ctx.String(flgHTTPFrostFSEndpoint), + ctx.String(flgHTTPFrostFSContainer), + ctx.String(flgHTTPFrostFSWallet), + ctx.String(flgHTTPFrostFSWalletAccount), + ctx.String(flgHTTPFrostFSWalletPass), + ) + if err != nil { + log.Fatal(err) + } + return ps case ctx.IsSet(flgHTTPPort): iface := ctx.String(flgHTTPPort) if !strings.Contains(iface, ":") { diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 94275c06..6b8b1f27 100644 --- a/docs/data/zz_cli_help.toml +++ b/docs/data/zz_cli_help.toml @@ -36,6 +36,11 @@ GLOBAL OPTIONS: --http.webroot value Set the webroot folder to use for HTTP-01 based challenges to write directly to the .well-known/acme-challenge file. This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge --http.memcached-host value [ --http.memcached-host value ] Set the memcached host(s) to use for HTTP-01 based challenges. Challenges will be written to all specified hosts. --http.s3-bucket value Set the S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket. + --http.frostfs-endpoint value Set FrostFS endpoint to use for HTTP-01 based challenges. Challenges will be written to FrostFS container [$FROSTFS_ENDPOINT] + --http.frostfs-container value Set FrostFS container ID to use for HTTP-01 based challenges. Challenges will be written to FrostFS container [$FROSTFS_CONTAINER] + --http.frostfs-wallet value Path to NEO wallet to use for interaction with FrostFS. If no wallet is provided an ephemeral one will be generated. Such key will only work for publicly writable containers and will significantly reduce security: any attacker with knowledge of FrostFS endpoint and CID will be able to obtain certificates for your domain [$FROSTFS_WALLET] + --http.frostfs-wallet-account value Wallet account to use for interaction with FrostFS. If not set, the first account from wallet will be used [$FROSTFS_WALLET_ACCOUNT] + --http.frostfs-wallet-password value Account password to decrypt the wallet. If not set, an empty password is assumed [$FROSTFS_WALLET_PASSWORD] --tls Use the TLS-ALPN-01 challenge to solve challenges. Can be mixed with other types of challenges. (default: false) --tls.port value Set the port and interface to use for TLS-ALPN-01 based challenges to listen on. Supported: interface:port or :port. (default: ":443") --dns value Solve a DNS-01 challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage. diff --git a/providers/http/frostfs/frostfs.go b/providers/http/frostfs/frostfs.go new file mode 100644 index 00000000..40b2b7a0 --- /dev/null +++ b/providers/http/frostfs/frostfs.go @@ -0,0 +1,64 @@ +// Package frostfs provides HTTP-01 solver that saves challenge token to +// FrostFS to make it available to multiple hosts at once. +// Useful for deploying FrostFS gateways (HTTP or S3) +package frostfs + +import ( + "context" + "errors" + "fmt" + + "github.com/go-acme/lego/v4/challenge" +) + +// HTTPProvider is a custom solver for HTTP-01 challenge that saves token to FrostFS. +type HTTPProvider struct { + frostfs *Storage + oid string +} + +var _ challenge.Provider = new(HTTPProvider) + +func NewHTTPProvider(endpoint, cid, walletPath, walletAccount, walletPassword string) (*HTTPProvider, error) { + if endpoint == "" { + return nil, errors.New("empty endpoint") + } + if cid == "" { + return nil, errors.New("empty container id") + } + key, err := getKey(walletPath, walletAccount, walletPassword) + if err != nil { + return nil, err + } + storage, err := Open(endpoint, cid, key) + if err != nil { + return nil, err + } + return &HTTPProvider{frostfs: storage}, nil +} + +func (w *HTTPProvider) Present(domain, token, keyAuth string) error { + var err error + if w.oid != "" { + return fmt.Errorf("%T is not safe to re-enter: object was saved and not yet cleaned up: %s", w, w.oid) + } + w.oid, err = w.frostfs.Save( + context.TODO(), + []byte(keyAuth), + "FileName", token, + "ACME", token, + ) + return err +} + +func (w *HTTPProvider) CleanUp(domain, token, keyAuth string) error { + if w.oid == "" { + panic("Cleanup() called before Present()") + } + err := w.frostfs.Delete(context.TODO(), w.oid) + if err != nil { + return err + } + w.oid = "" + return nil +}