Skip to content
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Configuration variables:
- `ACME_DNS_PROPAGATION_REQUIREMENT` - if set to true, requires complete DNS record propagation before stating that challenge is solved. Default: true
- `ACME_REREGISTER_ACCOUNT` - if set to true, allows registering an account with CA. This should be set to true for the first use. When credentials are stored in Vault, you can set this to false to avoid accidental registrations. Default: false
- `ACME_SERVER_URL` - ACME directory location. Default: https://acme-staging-v02.api.letsencrypt.org/directory
- `ACME_EAB_KID` - ACME External Account Binding Key Identifier. Optional.
- `ACME_EAB_HMAC_KEY` - ACME External Account Binding HMAC Key. Optional.
- `VAULT_APPROLE_ROLE_ID` - role ID for Vault approle authentication method. **Required in prod env**
- `VAULT_APPROLE_SECRET_ID` - secret ID for Vault approle authentication method. **Required in prod env**
- `VAULT_KV_STORAGE_PATH` - path in Vault KV storage where certificator stores certificates and account data. Default: secret/data/certificator/
Expand Down
3 changes: 1 addition & 2 deletions cmd/certificator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ func main() {
logger.Fatal(err)
}

acmeClient, err := acme.NewClient(cfg.Acme.AccountEmail, cfg.Acme.ServerURL,
cfg.Acme.ReregisterAccount, vaultClient, logger)
acmeClient, err := acme.NewClient(cfg.Acme, vaultClient, logger)
if err != nil {
logger.Fatal(err)
}
Expand Down
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ services:
# ports:
# - "14000:14000" # HTTPS ACME API
# - "15000:15000" # HTTPS Management API
pebble-eab:
image: letsencrypt/pebble:latest
command: pebble -config /test/config/pebble-config-external-account-bindings.json -strict -dnsserver challtestsrv:8053
volumes:
- ./fixtures/pebble-eab/cert.pem:/test/certs/localhost/cert.pem:Z
- ./fixtures/pebble-eab/key.pem:/test/certs/localhost/key.pem:Z
challtestsrv:
image: letsencrypt/pebble-challtestsrv:latest
depends_on:
- pebble
- pebble-eab
command: pebble-challtestsrv -http01 "" -tlsalpn01 "" -dns01 ":8053"
ports:
- "8055:8055" # HTTP Management API
Expand All @@ -25,6 +32,7 @@ services:
build: .
depends_on:
- pebble
- pebble-eab
- challtestsrv
- vault
env_file:
Expand All @@ -36,6 +44,7 @@ services:
dockerfile: Dockerfile.tester
depends_on:
- pebble
- pebble-eab
- challtestsrv
- vault
command: ["true"] # do not start the container when `docker-compose up` is executed
21 changes: 21 additions & 0 deletions fixtures/pebble-eab/cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIUDMCvZoyIm0luDchyjAX438QRODEwDQYJKoZIhvcNAQEL
BQAwIDEeMBwGA1UEAxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMB4XDTI1MTExMzE3
NTAxM1oXDTM1MTExMTE3NTAxM1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmuh8Ij6woMEinHbutHHf8M6QK8RV
mYwxDrZsS786FbNruOkxGMTSEsTGXsWZQl++WrGnNnbau6BdY2Na7eS+CRu/POZr
sV/mCFsqoCcDCdMdl8cKqKRdkn4jz7BOAVdZMFvBkZMlJLnzmXajMCkceHfDP9M7
PiwceLkkJ5stnfsdNna3BMdDQXwwNHGBG+ENI707rs25arT9L0+dLD4gXNUBFPaH
/myMMnFZrE4Wh9TasI/1lqcmsWrA+CAUvt1SGqlfWZfAMrNp+u1KdESp+CfwI+0F
y0KJ+LmIMWVEeTJiE1IaXxiwNXLndNODl6shQ92bKQAn2PPR/f3mf9/epwIDAQAB
o4G0MIGxMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUF
BwMBMCgGA1UdEQQhMB+CCWxvY2FsaG9zdIIGcGViYmxlggpwZWJibGUtZWFiMB0G
A1UdDgQWBBQHS0Usia66JfBmckioDedWLYYQWDA5BgNVHSMEMjAwoSSkIjAgMR4w
HAYDVQQDExVtaW5pY2Egcm9vdCBjYSAyNGUyZGKCCCTi23rPLE6+MA0GCSqGSIb3
DQEBCwUAA4IBAQBG0K1AnLPBL1yowiMUoGYpB8SI6J+yTuFDo582C9eJxhJG5TTH
AgTv/Ily/wU805yhw1afyq/eNomECMybrJ83DukQGNLoWjbqCLlZNo7ngyy7VeOA
jMiDgzAbQzLmhJqjOOa4SORWxMOIokp0wgrV3eB9OyxuxKmgio5MuCP5xZY57Hxt
HS3WBzHf+OqmD8obwrPdkrB3wxosiNAs96wsXCgExdOK8lWvZqC+3jId+EadSdCU
hGhEeMfhkn19o+5W+dhCdpWHMCtus3/X5lwo+RGlLTn9tB+7DLp/RT1UIanQgxT0
f5jAWybkXoSSj1AOJupCojLailyLQeqRs1eC
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions fixtures/pebble-eab/key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCa6HwiPrCgwSKc
du60cd/wzpArxFWZjDEOtmxLvzoVs2u46TEYxNISxMZexZlCX75asac2dtq7oF1j
Y1rt5L4JG7885muxX+YIWyqgJwMJ0x2XxwqopF2SfiPPsE4BV1kwW8GRkyUkufOZ
dqMwKRx4d8M/0zs+LBx4uSQnmy2d+x02drcEx0NBfDA0cYEb4Q0jvTuuzblqtP0v
T50sPiBc1QEU9of+bIwycVmsThaH1Nqwj/WWpyaxasD4IBS+3VIaqV9Zl8Ays2n6
7Up0RKn4J/Aj7QXLQon4uYgxZUR5MmITUhpfGLA1cud004OXqyFD3ZspACfY89H9
/eZ/396nAgMBAAECggEABoOoVLhWLSxyDCpkN2pihHnrTivffamLlpbNTxInmyoE
aa5Exkngm7FytiGRJF1qX94AriWpZSKFStyuwjpQU4m7LRBzju39enR7Amkhk5Qx
NmTEafsfhD2Ru/YqEZxev83cAgcyFK1NNTLsAeZ4sAg5ScHUR/UK7Xi7sH6yGGaa
OfNZf+2JXDSIQhZ5wKfP9UMLw7Pnk/tiTptWvAhhssHzUjeTZWOB0YcNLtdjMry0
a4Vgo1VEnTGlaazBRZrphcnkYz0L9TdZi3MBowFpwSAVaFnIYBVj0yMzUob7wMR4
rQ13oDB0cY6diSSBBcYAdcU5tl3Rgwe674M5r47OYQKBgQDSKNgsPTB5SWF4XPqR
pbzpoD/OCT9JWs2KfubkaYjT7W8vKt1a1+0NplcO8v9fAFaVraLWz2QWBcgBA2AT
3P969RRwq5RozujpKtlaq3np5sx6zLOV/EraCTrBAWG7bhnQ45NiuOZ0o4twn0h+
bn6cOg9WmAv5I6yUrp453OKdjQKBgQC8snFDgjK4t0dC7j83n5wX5U0H9Rl3IdXF
u2ccZSiw1ZuAh14iqEWbD404tqdUIJUDOb/Gp4C/cXaqGPX5+aD8m8o725Sxd4wy
htSr+Y2+t6RrRLjP3gmHwkGdacMLe0i8JSR1uv7mTCO9c6KZMuzG0wfoFmKLhH1N
uo1DmaeeAwKBgDa9+ukzGyBMs/31QjOUF2xHmLcjg3+pDmcn/R68Oq7xZwfDQ5t5
KT9n979mHozxepxMeWE47loqMVnKNkmWRFoT4roih2rxkovB5JIQnpYFz3ehQJot
+JVo2AwUPw/icwkNboPFpAn328d/RhPg1kv9d6dXva6s0zR7c62g0sq1AoGATTLD
29acYa253cwS2PLXrX8GMMc5j9+r0aWF3bwVFaOOZtr6zDsDnxiRsi4n7ucVj6hE
qUVGCwyke5qSp55VGoOyDSMchi5ekR1QTLtgHjKLdcc7xO8V/xCeP4RRY5UggLjB
XdkPB7EGvi9cykhnTkBv76eY0XN2wCIeQRXP2XkCgYAxF8s7syOR9ViPPMKxH/xM
GULvgMmXq4qvz+mi/n1Tnu/7NJPy34SR+nZvGdaKXwng2cCa8qlUrHlQod1hPsaT
6L8TT/4umoNGJdujqjjLML5reeqRQJGrQiULhZAajYGXTAKi84H2YPcK+60mFcKg
j27RTCjgFLvRubrM2fpgEg==
-----END PRIVATE KEY-----
34 changes: 21 additions & 13 deletions pkg/acme/acme.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"github.com/sirupsen/logrus"
"github.com/vinted/certificator/pkg/config"
"github.com/vinted/certificator/pkg/vault"
)

Expand Down Expand Up @@ -40,22 +41,21 @@ func (u *User) GetPrivateKey() crypto.PrivateKey {

// NewClient initializes acme client and returns
func NewClient(
email, serverURL string,
reregister bool,
cfg config.Acme,
vault *vault.VaultClient,
logger *logrus.Logger) (*lego.Client, error) {

acc, err := setupAccount(email, reregister, vault, logger)
acc, err := setupAccount(cfg.AccountEmail, cfg.ReregisterAccount, vault, logger)
if err != nil {
return nil, err
}

client, err := setupClient(acc, serverURL, logger)
client, err := setupClient(acc, cfg.ServerURL, logger)
if err != nil {
return nil, err
}

return registerAccount(acc, client, vault, serverURL, reregister, logger)
return registerAccount(acc, client, vault, cfg, logger)
}

func setupClient(
Expand Down Expand Up @@ -156,13 +156,13 @@ func getAccountKey(reregister bool, vault *vault.VaultClient, logger *logrus.Log
}

func registerAccount(acc *User, client *lego.Client, vault *vault.VaultClient,
serverURL string, reregister bool, logger *logrus.Logger) (*lego.Client, error) {
cfg config.Acme, logger *logrus.Logger) (*lego.Client, error) {
logger.Debug("checking client registration")
_, err := client.Registration.QueryRegistration()
if err != nil {
logger.Warn("registration not found")

client, err = recoverAccount(acc, client, vault, serverURL, reregister, logger)
client, err = recoverAccount(acc, client, vault, cfg, logger)
if err != nil {
return nil, err
}
Expand All @@ -174,26 +174,34 @@ func registerAccount(acc *User, client *lego.Client, vault *vault.VaultClient,
}

func recoverAccount(acc *User, client *lego.Client, vault *vault.VaultClient,
serverURL string, reregister bool, logger *logrus.Logger) (*lego.Client, error) {
cfg config.Acme, logger *logrus.Logger) (*lego.Client, error) {
// Try to resolve registration by private key
reg, err := client.Registration.ResolveAccountByKey()

if err != nil {
logger.Warn("could not resolve account by key")

if reregister {
if cfg.ReregisterAccount {
// Reset local registration data and reregister
logger.Info("reregistering account")
acc.Registration = nil
client, err = setupClient(acc, serverURL, logger)
client, err = setupClient(acc, cfg.ServerURL, logger)
if err != nil {
return nil, err
}

reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
return nil, err
if cfg.EABKid != "" && cfg.EABHmacKey != "" {
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{TermsOfServiceAgreed: true, Kid: cfg.EABKid, HmacEncoded: cfg.EABHmacKey})
if err != nil {
return nil, err
}
} else {
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
return nil, err
}
}

acc.Registration = reg
} else {
return nil, errors.New("account registration not found and re-registering is disabled")
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type Acme struct {
DNSPropagationRequirement bool `envconfig:"ACME_DNS_PROPAGATION_REQUIREMENT" default:"true"`
ReregisterAccount bool `envconfig:"ACME_REREGISTER_ACCOUNT" default:"false"`
ServerURL string `envconfig:"ACME_SERVER_URL" default:"https://acme-staging-v02.api.letsencrypt.org/directory"`
EABKid string `envconfig:"ACME_EAB_KID"`
EABHmacKey string `envconfig:"ACME_EAB_HMAC_KEY"`
}

// Vault contains vault related configuration parameters
Expand Down
69 changes: 60 additions & 9 deletions test/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,23 @@ import (
"github.com/thanos-io/thanos/pkg/testutil"
"github.com/vinted/certificator/pkg/acme"
"github.com/vinted/certificator/pkg/certificate"
"github.com/vinted/certificator/pkg/config"
"github.com/vinted/certificator/pkg/vault"
)

var (
// vaultDevToken token should be equal to `VAULT_DEV_ROOT_TOKEN_ID` set in vault container
// It should be defined in docker-compoose.yml
vaultDevToken string = "supersecret"
vaultKVPath string = "/secret/data/integration_test/"
acc *acme.User
keyEncoded string
acmeEmail string = "test@test.com"
acmeURL string = "https://pebble:14000/dir"
vaultDevToken string = "supersecret"
vaultKVPath string = "/secret/data/integration_test/"
vaultEABKVPath string = "/secret/data/integration_test_eab/"
acc *acme.User
keyEncoded string
acmeEmail string = "test@test.com"
acmeURL string = "https://pebble:14000/dir"
acmeEABURL string = "https://pebble-eab:14000/dir"
acmeEABKID string = "kid-1"
acmeEABHMACKEY string = "zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W"
)

func TestMain(m *testing.M) {
Expand Down Expand Up @@ -55,8 +60,14 @@ func TestAcmeClientAndAccountSetup(t *testing.T) {
deleteAccountFromVault(t, testVaultClient)
deleteKeyFromVault(t, testVaultClient)

acmeConfig := config.Acme{
AccountEmail: acmeEmail,
ServerURL: acmeURL,
ReregisterAccount: true,
}

// This populates data in Vault, account and key are both present
_, err = acme.NewClient(acmeEmail, acmeURL, true, vaultClient, logger)
_, err = acme.NewClient(acmeConfig, vaultClient, logger)
testutil.Ok(t, err)

// Save account and key data from first registration
Expand Down Expand Up @@ -132,7 +143,12 @@ func TestAcmeClientAndAccountSetup(t *testing.T) {
testutil.Ok(t, err)
}

_, err := acme.NewClient(acmeEmail, acmeURL, tcase.reregisteringEnabled, vaultClient, logger)
acmeConfig := config.Acme{
AccountEmail: acmeEmail,
ServerURL: acmeURL,
ReregisterAccount: tcase.reregisteringEnabled,
}
_, err := acme.NewClient(acmeConfig, vaultClient, logger)
if tcase.expectedErr {
testutil.NotOk(t, err)
} else {
Expand All @@ -149,7 +165,12 @@ func TestCertificateObtaining(t *testing.T) {
vaultClient, err := vault.NewVaultClient("", "", "dev", vaultKVPath, logger)
testutil.Ok(t, err)

acmeClient, err := acme.NewClient(acmeEmail, acmeURL, true, vaultClient, logger)
acmeConfig := config.Acme{
AccountEmail: acmeEmail,
ServerURL: acmeURL,
ReregisterAccount: true,
}
acmeClient, err := acme.NewClient(acmeConfig, vaultClient, logger)
testutil.Ok(t, err)

for _, domain := range []string{"example.com", "test.com", "mydomain.com"} {
Expand All @@ -165,6 +186,36 @@ func TestCertificateObtaining(t *testing.T) {
}
}

func TestCertificateObtainingWithEAB(t *testing.T) {
logger := logrus.New()
logger.SetLevel(logrus.WarnLevel)

vaultClient, err := vault.NewVaultClient("", "", "dev", vaultEABKVPath, logger)
testutil.Ok(t, err)

acmeConfig := config.Acme{
ServerURL: acmeEABURL,
ReregisterAccount: true,
EABKid: acmeEABKID,
EABHmacKey: acmeEABHMACKEY,
}

acmeClient, err := acme.NewClient(acmeConfig, vaultClient, logger)
testutil.Ok(t, err)

for _, domain := range []string{"example-eab.com", "test-eab.com", "mydomain-eab.com"} {
err := certificate.ObtainCertificate(acmeClient, vaultClient, []string{domain},
"challtestsrv:8053", "exec", false)
testutil.Ok(t, err)

cert, err := certificate.GetCertificate(domain, vaultClient)
testutil.Ok(t, err)

// Check if certificate is issued recently
testutil.Assert(t, time.Since(cert.NotBefore).Minutes() < 5)
}
}

func deleteAccountFromVault(t *testing.T, cl *api.Client) {
t.Log("Deleting account from Vault")
_, err := cl.Logical().Delete(vaultKVPath + "account")
Expand Down