WIP: HTTP-01 solver that stores challenge tokens in FrostFS #4

Draft
potyarkin wants to merge 9 commits from potyarkin/lego:feature/http-01-frostfs-solver into tcl/http-01-frostfs-solver
4 changed files with 112 additions and 0 deletions
Showing only changes of commit d8c8aba312 - Show all commits

View file

@ -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.",

View file

@ -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, ":") {

View file

@ -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.

View file

@ -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
}