Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement OAuth and patch CN bug #22

Merged
merged 15 commits into from
Jul 19, 2024
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Keyfactor Release Workflow
name: Keyfactor Bootstrap Workflow

on:
workflow_dispatch:
Expand All @@ -11,9 +11,10 @@ on:

jobs:
call-starter-workflow:
uses: keyfactor/actions/.github/workflows/starter.yml@v2
uses: keyfactor/actions/.github/workflows/starter.yml@v3
secrets:
token: ${{ secrets.V2BUILDTOKEN}}
APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}}
gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }}
gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }}
scan_token: ${{ secrets.SAST_TOKEN }}
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ jobs:
with:
deploy-k8s: 'true'
deploy-nginx-ingress: 'true'
deploy-signserver: 'false'

# Run Go tests
- name: Run go test
Expand Down
30 changes: 30 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
run:
# timeout for analysis, e.g. 30s, 5m, default is 1m
timeout: 12m

skip-dirs:
- testdata$
- test/mock

skip-files:
- ".*\\.pb\\.go"

linters:
enable:
- bodyclose
- errorlint
- goimports
- revive
- gosec
- misspell
- nakedret
- unconvert
- unparam
- whitespace
- gocritic
- nolintlint

linters-settings:
revive:
# minimal confidence for issues, default is 0.8
confidence: 0.0
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# v1.5.0
## Fixes
* Patched bug that required the `allow_any_name` or `allowed_domains=[""]` role parameters to issue/sign certificates with no CN.

## Chores
* Upgrade from Go `v1.21` to `v1.22`.
* Implement more strict golangci-lint policy.

## Features
* If upstream EJBCA API call fails, the HTTP status code is propogated to the Vault user via the Vault API status code.
* Implement OAuth 2.0 "client credentials" token flow as a supported authentication mechanism to EJBCA.

# v1.4.0
## Fixes
* Paths that need to write to the Storage backend now forward the request to the primary node. This is important in Enterprise HA deployments where Performance Standby Nodes are allowed to handle read requests/paths.
Expand Down
84 changes: 23 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,15 @@

# EJBCA Vault PKI Secrets Engine

EJBCA PKI Engine and Backend for HashiCorp Vault. Used to issue, sign, and revoke certificates using the EJBCA CA via HashiCorp Vault

#### Integration status: Production - Ready for use in production environments.

## About the Keyfactor API Client

This API client allows for programmatic management of Keyfactor resources.

## Support for EJBCA Vault PKI Secrets Engine

EJBCA Vault PKI Secrets Engine is open source and supported on best effort level for this tool/library/client. This means customers can report Bugs, Feature Requests, Documentation amendment or questions as well as requests for customer information required for setup that needs Keyfactor access to obtain. Such requests do not follow normal SLA commitments for response or resolution. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com/

###### To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab.

---


---


# EJBCA Vault PKI Secrets Engine

<!--EJBCA Community logo -->
<a href="https://ejbca.org">
<img src=".github/images/community-ejbca.png?raw=true)" alt="EJBCA logo" title="EJBCA" height="70" />
</a>
<!--EJBCA Enterprise logo -->
<a href="https://www.keyfactor.com/products/ejbca-enterprise/">
<img src=".github/images/keyfactor-ejbca-enterprise.png?raw=true)" alt="EJBCA logo" title="EJBCA" height="70" />
</a>
![Integration Status: production](https://img.shields.io/badge/integration_status-production-3D1973?style=flat-square)
[![Valid for EJBCA Community](https://img.shields.io/badge/valid_for-ejbca_community-FF9371)](https://ejbca.org)
[![Valid for EJBCA Enterprise](https://img.shields.io/badge/valid_for-ejbca_enterprise-5F61FF)](https://www.keyfactor.com/products/ejbca-enterprise/)
[![Go Report Card](https://goreportcard.com/badge/github.com/keyfactor/ejbca-vault-pki-engine)](https://goreportcard.com/report/github.com/keyfactor/ejbca-vault-pki-engine)

<!--- Insert the Tool Name in the main heading! --->
# EJBCA PKI Secrets Engine for HashiCorp Vault

[![Go Report Card](https://goreportcard.com/badge/github.com/Keyfactor/ejbca-vault-pki-engine)](https://goreportcard.com/report/github.com/Keyfactor/ejbca-vault-pki-engine)

<!--- Short intro here! --->
<!--- Include a description of the project/repository, the purpose of it, what problems it solves, when to use it (and not use it), etc. --->
## Overview

The EJBCA PKI Secrets Engine for HashiCorp Vault enables DevOps teams to request and retrieve certificates
from EJBCA using HashiCorp Vault, while security teams retain control over backend PKI operations.
Expand All @@ -48,49 +20,39 @@ The EJBCA PKI Secrets Engine is a Vault plugin that replicates the built-in Vaul
requests through EJBCA instead of through Vault. The plugin was designed to be swapped for the built-in Vault PKI secrets engine
with minimal changes to existing Vault configurations.

## Get Started

<!--- Insert links to instructions on how to install, configure, etc.
Example from ejbca-cert-manager-issuer below:

* To install the tool, see [Installation](docs/install.md).
* To configure and use the tool, see:
* [Usage](docs/config_usage.md)
* [Customization](docs/annotations.md)
* [End Entity Name Selection](docs/endentitynamecustomization.md)
* To test the tool, see [Testing the Source](docs/testing.md).
--->
## Requirements

To get started with EJBCA PKI Secrets Engine for HashiCorp Vault, see [Getting Started](docs/getting-started.md).
### To build
* [Git](https://git-scm.com/)
* [Golang](https://golang.org/) >= v1.22

### System Requirements
### To use
* [EJBCA](https://www.keyfactor.com/products/ejbca-enterprise/) >= v7.7
* [HashiCorp Vault](https://www.vaultproject.io/) >= v1.11.0



## Getting Started

To get started with EJBCA PKI Secrets Engine for HashiCorp Vault, see [Getting Started](docs/getting-started.md).

<!--- Insert any requirements in this section. --->
To run the EJBCA PKI Secrets Engine for HashiCorp Vault, the EJBCA REST API needs to be set up with certain endpoints. There are also requirements on certain versions of Git, Golang, EJBCA, and HashiCorp Vault.

See the complete list in [System Requirements](docs/getting-started.md#requirements).

## Community Support
In the [Keyfactor Community](https://www.keyfactor.com/community/), we welcome contributions.

The Community software is open-source and community-supported, meaning that **no SLA** is applicable.
In the [Keyfactor Community](https://www.keyfactor.com/community/), we welcome contributions. Keyfactor Community software is open-source and community-supported, meaning that **no SLA** is applicable. Keyfactor will address issues as resources become available.

* To report a problem or suggest a new feature, go to [Issues](../../issues).
* If you want to contribute actual bug fixes or proposed enhancements, see the [Contributing Guidelines](CONTRIBUTING.md) and go to [Pull requests](../../pulls).
* If you want to contribute bug fixes or proposed enhancements, see the [Contributing Guidelines](CONTRIBUTING.md) and create a [Pull request](../../pulls).

## Commercial Support

Commercial support is available for [EJBCA Enterprise](https://www.keyfactor.com/products/ejbca-enterprise/).

<!--- For SignServer, update to the following text and link:
Commercial support is available for [SignServer Enterprise](https://www.keyfactor.com/products/signserver-enterprise/).
--->

## License
<!--- No updates needed --->
For License information, see [LICENSE](LICENSE).
For license information, see [LICENSE](LICENSE).

## Related Projects
See all [Keyfactor EJBCA GitHub projects](https://github.com/orgs/Keyfactor/repositories?q=ejbca).


See all [Keyfactor EJBCA GitHub projects](https://github.com/orgs/Keyfactor/repositories?q=ejbca).
136 changes: 119 additions & 17 deletions backend.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
/*
Copyright 2024 Keyfactor
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain a
copy of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless
required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
OR CONDITIONS OF ANY KIND, either express or implied. See the License for
thespecific language governing permissions and limitations under the
License.
Copyright © 2024 Keyfactor

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package ejbca_vault_pki_engine

package ejbca

import (
"context"
"math/rand"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"fmt"
"math/big"
"strings"
"sync"

"github.com/Keyfactor/ejbca-go-client-sdk/api/ejbca"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/errutil"
"github.com/hashicorp/vault/sdk/logical"
)

Expand Down Expand Up @@ -96,7 +107,7 @@ func (b *ejbcaBackend) reset() {

// invalidate clears an existing client configuration in
// the backend
func (b *ejbcaBackend) invalidate(ctx context.Context, key string) {
func (b *ejbcaBackend) invalidate(_ context.Context, key string) {
if key == "config" {
b.reset()
}
Expand Down Expand Up @@ -130,17 +141,104 @@ func (sc *storageContext) getClient() (*ejbcaClient, error) {
config = new(ejbcaConfig)
}

logger.Trace("Creating new EJBCA authenticator")
authenticator, err := sc.Backend.newAuthenticator(config)
if err != nil {
return nil, err
}
if authenticator == nil {
logger.Error("Authenticator is nil")
return nil, fmt.Errorf("Authenticator is nil")
}

logger.Trace("Creating new EJBCA client")
sc.Backend.client, err = newClient(config)
sdkConfig := ejbca.NewConfiguration()
sdkConfig.Host = config.Hostname
sdkConfig.SetAuthenticator(authenticator)

client, err := ejbca.NewAPIClient(sdkConfig)
if err != nil {
return nil, err
}
sc.Backend.client = &ejbcaClient{client}

return sc.Backend.client, nil
}

func (b *ejbcaBackend) newAuthenticator(config *ejbcaConfig) (ejbca.Authenticator, error) {
var err error
logger := b.Logger().Named("ejbcaBackend.newAuthenticator")

var caChain []*x509.Certificate
if config.CaCert != "" {
logger.Info("CA chain present - Parsing CA chain from configuration")

blocks := decodePEMBytes([]byte(config.CaCert))
if len(blocks) == 0 {
return nil, errutil.UserError{Err: "didn't find pem certificate in ca_cert"}
}

for _, block := range blocks {
// Parse the PEM block into an x509 certificate
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse CA certificate: %w", err)
}

caChain = append(caChain, cert)
}

logger.Debug("Parsed CA chain", "length", len(caChain))
}

var authenticator ejbca.Authenticator
switch {
case config.ClientCert != "" && config.ClientKey != "":
logger.Info("Creating mTLS authenticator")

var tlsCert tls.Certificate
tlsCert, err := tls.X509KeyPair([]byte(config.ClientCert), []byte(config.ClientKey))
if err != nil {
return nil, fmt.Errorf("failed to load client certificate: %w", err)
}

authenticator, err = ejbca.NewMTLSAuthenticatorBuilder().
WithCaCertificates(caChain).
WithClientCertificate(&tlsCert).
Build()
if err != nil {
logger.Error("Failed to build mTLS authenticator")
return nil, fmt.Errorf("failed to build MTLS authenticator: %w", err)
}

logger.Info("Created mTLS authenticator")
case config.TokenURL != "" && config.ClientID != "" && config.ClientSecret != "":
logger.Info("Creating OAuth authenticator")

authenticator, err = ejbca.NewOAuthAuthenticatorBuilder().
WithCaCertificates(caChain).
WithTokenUrl(config.TokenURL).
WithClientId(config.ClientID).
WithClientSecret(config.ClientSecret).
WithAudience(config.Audience).
WithScopes(config.Scopes).
Build()
if err != nil {
logger.Error("Failed to build OAuth authenticator")
return nil, fmt.Errorf("failed to build OAuth authenticator: %w", err)
}

logger.Info("Created OAuth authenticator")
default:
logger.Error("no authentication method configured")
return nil, fmt.Errorf("no authentication method configured")
}

return authenticator, nil
}

func (b *ejbcaBackend) isRunningOnPerformanceStandby() bool {
return b.System().ReplicationState().HasState(consts.ReplicationPerformanceStandby)
return b.System().ReplicationState().HasState(consts.ReplicationPerformanceStandby)
}

// backendHelp should contain help information for the backend
Expand All @@ -150,11 +248,15 @@ After mounting this backend, credentials to manage certificates must be configur
with the "config/" endpoints.
`

func generateRandomString(length int) string {
func generateRandomString(length int) (string, error) {
letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, length)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
return "", err
}
b[i] = letters[num.Int64()]
}
return string(b)
return string(b), nil
}
Loading
Loading