mirror of
https://github.com/absmach/supermq.git
synced 2026-06-23 06:30:22 +00:00
SMQ-3201 - Replace Vault with openbao (#3019)
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
Continuous Delivery / Build and Push (push) Has been cancelled
Check the consistency of generated files / check-generated-files (push) Has been cancelled
Check License Header / check-license (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
Continuous Delivery / Build and Push (push) Has been cancelled
Check the consistency of generated files / check-generated-files (push) Has been cancelled
Check License Header / check-license (push) Has been cancelled
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
This commit is contained in:
@@ -61,7 +61,7 @@ jobs:
|
||||
- "users/emailer.go"
|
||||
- "users/hasher.go"
|
||||
- "certs/certs.go"
|
||||
- "certs/pki/vault.go"
|
||||
- "certs/pki/openbao/openbao.go"
|
||||
- "certs/service.go"
|
||||
- "journal/journal.go"
|
||||
- "consumers/notifier.go"
|
||||
|
||||
+2
-2
@@ -16,5 +16,5 @@ coverage
|
||||
# Schemathesis
|
||||
.hypothesis
|
||||
|
||||
# Ignore Vault data directory as it contains runtime-generated data
|
||||
docker/addons/vault/data/
|
||||
# Ignore Openbao data directory as it contains runtime-generated data
|
||||
docker/addons/certs/openbao/
|
||||
|
||||
@@ -74,7 +74,7 @@ endef
|
||||
|
||||
ADDON_SERVICES = journal certs
|
||||
|
||||
EXTERNAL_SERVICES = vault prometheus
|
||||
EXTERNAL_SERVICES = prometheus
|
||||
|
||||
ifneq ($(filter run%,$(firstword $(MAKECMDGOALS))),)
|
||||
temp_args := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
||||
|
||||
@@ -81,11 +81,41 @@ paths:
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
operationId: revokeCert
|
||||
summary: Revokes a certificate
|
||||
|
||||
/{domainID}/certs/{clientID}/revoke-all:
|
||||
post:
|
||||
operationId: revokeAllCerts
|
||||
summary: Revokes all certificates for a given client ID
|
||||
description: |
|
||||
Revokes a certificate for a given cert ID.
|
||||
Revokes all certificates for a given client ID.
|
||||
tags:
|
||||
- certs
|
||||
parameters:
|
||||
- $ref: "auth.yaml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ClientID"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/RevokeRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: |
|
||||
Failed to revoke corresponding certificate.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/certs/{certID}/revoke:
|
||||
post:
|
||||
operationId: revokeCertBySerial
|
||||
summary: Revokes a certificate by serial number
|
||||
description: |
|
||||
Revokes a certificate for a given certificate serial number.
|
||||
tags:
|
||||
- certs
|
||||
parameters:
|
||||
@@ -164,7 +194,7 @@ components:
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "7c:a9:91:e7:13:e9:5c:6b:1d:16:cf:76:20:82:f3:01:c3:d5:a6:66"
|
||||
required: true
|
||||
|
||||
schemas:
|
||||
@@ -245,7 +275,7 @@ components:
|
||||
description: |
|
||||
Issues a certificate that is required for mTLS. To create a certificate for a client
|
||||
provide a client id, data identifying particular client will be embedded into the Certificate.
|
||||
x509 and ECC certificates are supported when using when Vault is used as PKI.
|
||||
x509 and ECC certificates are supported when using when Openbao is used as PKI.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
|
||||
+24
-24
@@ -1,23 +1,23 @@
|
||||
# Certs Service
|
||||
|
||||
Issues certificates for clients. `Certs` service can create certificates to be used when `SuperMQ` is deployed to support mTLS.
|
||||
Certificate service can create certificates using PKI mode - where certificates issued by PKI, when you deploy `Vault` as PKI certificate management `cert` service will proxy requests to `Vault` previously checking access rights and saving info on successfully created certificate.
|
||||
Certificate service can create certificates using PKI mode - where certificates issued by PKI, when you deploy `OpenBao` as PKI certificate management `cert` service will proxy requests to `OpenBao` previously checking access rights and saving info on successfully created certificate.
|
||||
|
||||
## PKI mode
|
||||
|
||||
When `SMQ_CERTS_VAULT_HOST` is set it is presumed that `Vault` is installed and `certs` service will issue certificates using `Vault` API.
|
||||
First you'll need to set up `Vault`.
|
||||
To setup `Vault` follow steps in [Build Your Own Certificate Authority (CA)](https://learn.hashicorp.com/tutorials/vault/pki-engine).
|
||||
When `SMQ_CERTS_OPENBAO_HOST` is set it is presumed that `OpenBao` is installed and `certs` service will issue certificates using `OpenBao` API.
|
||||
First you'll need to set up `OpenBao`.
|
||||
To setup `OpenBao` follow steps in the [OpenBao PKI Documentation](https://openbao.org/docs/secrets/pki/).
|
||||
|
||||
For lab purposes you can use docker-compose and script for setting up PKI in [https://github.com/absmach/supermq/blob/main/docker/addons/vault/README.md](https://github.com/absmach/supermq/blob/main/docker/addons/vault/README.md)
|
||||
For lab purposes you can use docker-compose and script for setting up PKI in [https://github.com/absmach/supermq/blob/main/docker/addons/certs/README.md](https://github.com/absmach/supermq/blob/main/docker/addons/certs/README.md)
|
||||
|
||||
```bash
|
||||
SMQ_CERTS_VAULT_HOST=<https://vault-domain:8200>
|
||||
SMQ_CERTS_VAULT_NAMESPACE=<vault_namespace>
|
||||
SMQ_CERTS_VAULT_APPROLE_ROLEID=<vault_approle_roleid>
|
||||
SMQ_CERTS_VAULT_APPROLE_SECRET=<vault_approle_sceret>
|
||||
SMQ_CERTS_VAULT_CLIENTS_CERTS_PKI_PATH=<vault_clients_certs_pki_path>
|
||||
SMQ_CERTS_VAULT_CLIENTS_CERTS_PKI_ROLE_NAME=<vault_clients_certs_issue_role_name>
|
||||
SMQ_CERTS_OPENBAO_HOST=<https://openbao-domain:8200>
|
||||
SMQ_CERTS_OPENBAO_NAMESPACE=<openbao_namespace>
|
||||
SMQ_CERTS_OPENBAO_APP_ROLE=<openbao_app_role>
|
||||
SMQ_CERTS_OPENBAO_APP_SECRET=<openbao_app_secret>
|
||||
SMQ_CERTS_OPENBAO_PKI_PATH=<openbao_pki_path>
|
||||
SMQ_CERTS_OPENBAO_ROLE=<openbao_role_name>
|
||||
```
|
||||
|
||||
The certificates can also be revoked using `certs` service. To revoke a certificate you need to provide `client_id` of the client for which the certificate was issued.
|
||||
@@ -44,12 +44,12 @@ The service is configured using the environment variables presented in the follo
|
||||
| SMQ_AUTH_GRPC_SERVER_CERTS | Path to the PEM encoded auth server gRPC server trusted CA certificate file | "" |
|
||||
| SMQ_CERTS_SIGN_CA_PATH | Path to the PEM encoded CA certificate file | ca.crt |
|
||||
| SMQ_CERTS_SIGN_CA_KEY_PATH | Path to the PEM encoded CA key file | ca.key |
|
||||
| SMQ_CERTS_VAULT_HOST | Vault host | http://vault:8200 |
|
||||
| SMQ_CERTS_VAULT_NAMESPACE | Vault namespace in which pki is present | supermq |
|
||||
| SMQ_CERTS_VAULT_APPROLE_ROLEID | Vault AppRole auth RoleID | supermq |
|
||||
| SMQ_CERTS_VAULT_APPROLE_SECRET | Vault AppRole auth Secret | supermq |
|
||||
| SMQ_CERTS_VAULT_CLIENTS_CERTS_PKI_PATH | Vault PKI path for issuing Clients Certificates | pki_int |
|
||||
| SMQ_CERTS_VAULT_CLIENTS_CERTS_PKI_ROLE_NAME | Vault PKI Role Name for issuing Clients Certificates | supermq_clients_certs |
|
||||
| SMQ_CERTS_OPENBAO_HOST | OpenBao host | http://localhost:8200 |
|
||||
| SMQ_CERTS_OPENBAO_NAMESPACE | OpenBao namespace in which pki is present | "" |
|
||||
| SMQ_CERTS_OPENBAO_APP_ROLE | OpenBao AppRole auth RoleID | "" |
|
||||
| SMQ_CERTS_OPENBAO_APP_SECRET | OpenBao AppRole auth Secret | "" |
|
||||
| SMQ_CERTS_OPENBAO_PKI_PATH | OpenBao PKI path for issuing Clients Certificates | pki |
|
||||
| SMQ_CERTS_OPENBAO_ROLE | OpenBao PKI Role Name for issuing Clients Certificates | supermq |
|
||||
| SMQ_CERTS_DB_HOST | Database host | localhost |
|
||||
| SMQ_CERTS_DB_PORT | Database port | 5432 |
|
||||
| SMQ_CERTS_DB_PASS | Database password | supermq |
|
||||
@@ -69,7 +69,7 @@ The service is configured using the environment variables presented in the follo
|
||||
|
||||
The service is distributed as Docker container. Check the [`certs`](https://github.com/absmach/supermq/blob/main/docker/addons/certs/docker-compose.yaml) service section in docker-compose file to see how the service is deployed.
|
||||
|
||||
Running this service outside of container requires working instance of the auth service, clients service, postgres database, vault and Jaeger server.
|
||||
Running this service outside of container requires working instance of the auth service, clients service, postgres database, OpenBao and Jaeger server.
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
@@ -97,12 +97,12 @@ SMQ_AUTH_GRPC_CLIENT_KEY="" \
|
||||
SMQ_AUTH_GRPC_SERVER_CERTS="" \
|
||||
SMQ_CERTS_SIGN_CA_PATH=ca.crt \
|
||||
SMQ_CERTS_SIGN_CA_KEY_PATH=ca.key \
|
||||
SMQ_CERTS_VAULT_HOST=http://vault:8200 \
|
||||
SMQ_CERTS_VAULT_NAMESPACE=supermq \
|
||||
SMQ_CERTS_VAULT_APPROLE_ROLEID=supermq \
|
||||
SMQ_CERTS_VAULT_APPROLE_SECRET=supermq \
|
||||
SMQ_CERTS_VAULT_CLIENTS_CERTS_PKI_PATH=pki_int \
|
||||
SMQ_CERTS_VAULT_CLIENTS_CERTS_PKI_ROLE_NAME=supermq_clients_certs \
|
||||
SMQ_CERTS_OPENBAO_HOST=http://localhost:8200 \
|
||||
SMQ_CERTS_OPENBAO_NAMESPACE="" \
|
||||
SMQ_CERTS_OPENBAO_APP_ROLE=supermq \
|
||||
SMQ_CERTS_OPENBAO_APP_SECRET=supermq \
|
||||
SMQ_CERTS_OPENBAO_PKI_PATH=pki \
|
||||
SMQ_CERTS_OPENBAO_ROLE=supermq \
|
||||
SMQ_CERTS_DB_HOST=localhost \
|
||||
SMQ_CERTS_DB_PORT=5432 \
|
||||
SMQ_CERTS_DB_PASS=supermq \
|
||||
|
||||
+23
-4
@@ -27,6 +27,9 @@ func issueCert(svc certs.Service) endpoint.Endpoint {
|
||||
SerialNumber: res.SerialNumber,
|
||||
ClientID: res.ClientID,
|
||||
Certificate: res.Certificate,
|
||||
Key: res.Key,
|
||||
CAChain: res.CAChain,
|
||||
IssuingCA: res.IssuingCA,
|
||||
ExpiryTime: res.ExpiryTime,
|
||||
Revoked: res.Revoked,
|
||||
issued: true,
|
||||
@@ -58,8 +61,8 @@ func listSerials(svc certs.Service) endpoint.Endpoint {
|
||||
cr := certsRes{
|
||||
SerialNumber: cert.SerialNumber,
|
||||
ExpiryTime: cert.ExpiryTime,
|
||||
Revoked: cert.Revoked,
|
||||
ClientID: cert.ClientID,
|
||||
Revoked: cert.Revoked,
|
||||
}
|
||||
res.Certs = append(res.Certs, cr)
|
||||
}
|
||||
@@ -91,13 +94,29 @@ func viewCert(svc certs.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func revokeCert(svc certs.Service) endpoint.Endpoint {
|
||||
func revokeAllCerts(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(revokeReq)
|
||||
req := request.(revokeAllReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
res, err := svc.RevokeCert(ctx, req.domainID, req.token, req.certID)
|
||||
res, err := svc.RevokeCert(ctx, req.domainID, req.token, req.clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return revokeCertsRes{
|
||||
RevocationTime: res.RevocationTime,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func revokeBySerial(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(revokeBySerialReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
res, err := svc.RevokeBySerial(ctx, req.serialID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+117
-4
@@ -332,7 +332,7 @@ func TestViewCert(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevokeCert(t *testing.T) {
|
||||
func TestRevokeAllCerts(t *testing.T) {
|
||||
cs, svc, auth := newCertServer()
|
||||
defer cs.Close()
|
||||
|
||||
@@ -400,8 +400,8 @@ func TestRevokeCert(t *testing.T) {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
req := testRequest{
|
||||
client: cs.Client(),
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/%s/certs/%s", cs.URL, tc.domainID, tc.serialID),
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/%s/certs/%s/revoke-all", cs.URL, tc.domainID, tc.serialID),
|
||||
token: tc.token,
|
||||
}
|
||||
if tc.token == valid {
|
||||
@@ -425,6 +425,119 @@ func TestRevokeCert(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevokeBySerial(t *testing.T) {
|
||||
cs, svc, auth := newCertServer()
|
||||
defer cs.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
domainID string
|
||||
session smqauthn.Session
|
||||
serialID string
|
||||
status int
|
||||
authenticateErr error
|
||||
svcRes certs.Revoke
|
||||
svcErr error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "revoke cert by serial successfully",
|
||||
token: valid,
|
||||
domainID: valid,
|
||||
serialID: serial,
|
||||
status: http.StatusOK,
|
||||
svcRes: certs.Revoke{RevocationTime: time.Now()},
|
||||
svcErr: nil,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "revoke by serial with invalid token",
|
||||
token: invalid,
|
||||
domainID: valid,
|
||||
serialID: serial,
|
||||
status: http.StatusUnauthorized,
|
||||
svcRes: certs.Revoke{},
|
||||
authenticateErr: svcerr.ErrAuthentication,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "revoke by serial with empty domain id",
|
||||
token: valid,
|
||||
domainID: "",
|
||||
serialID: serial,
|
||||
status: http.StatusBadRequest,
|
||||
svcErr: nil,
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "revoke by serial with empty token",
|
||||
token: "",
|
||||
serialID: serial,
|
||||
domainID: valid,
|
||||
status: http.StatusUnauthorized,
|
||||
svcErr: nil,
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "revoke by serial with empty serial ID",
|
||||
token: valid,
|
||||
domainID: valid,
|
||||
serialID: "",
|
||||
status: http.StatusBadRequest,
|
||||
svcErr: nil,
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "revoke non-existing cert by serial",
|
||||
token: valid,
|
||||
domainID: valid,
|
||||
serialID: invalid,
|
||||
status: http.StatusNotFound,
|
||||
svcRes: certs.Revoke{},
|
||||
svcErr: svcerr.ErrNotFound,
|
||||
err: svcerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "revoke by serial with service error",
|
||||
token: valid,
|
||||
domainID: valid,
|
||||
serialID: serial,
|
||||
status: http.StatusUnprocessableEntity,
|
||||
svcRes: certs.Revoke{},
|
||||
svcErr: svcerr.ErrRemoveEntity,
|
||||
err: svcerr.ErrRemoveEntity,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
req := testRequest{
|
||||
client: cs.Client(),
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/%s/certs/%s/revoke", cs.URL, tc.domainID, tc.serialID),
|
||||
token: tc.token,
|
||||
}
|
||||
if tc.token == valid {
|
||||
tc.session = smqauthn.Session{DomainUserID: validID, UserID: validID, DomainID: validID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := svc.On("RevokeBySerial", mock.Anything, tc.serialID).Return(tc.svcRes, tc.svcErr)
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
var errRes respBody
|
||||
err = json.NewDecoder(res.Body).Decode(&errRes)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err))
|
||||
if errRes.Err != "" || errRes.Message != "" {
|
||||
err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message))
|
||||
}
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListSerials(t *testing.T) {
|
||||
cs, svc, auth := newCertServer()
|
||||
defer cs.Close()
|
||||
@@ -651,7 +764,7 @@ func TestListSerials(t *testing.T) {
|
||||
tc.session = smqauthn.Session{DomainUserID: validID, UserID: validID, DomainID: validID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := svc.On("ListSerials", mock.Anything, tc.clientID, certs.PageMetadata{Revoked: tc.revoked, Offset: tc.offset, Limit: tc.limit}).Return(tc.svcRes, tc.svcErr)
|
||||
svcCall := svc.On("ListSerials", mock.Anything, tc.clientID, certs.PageMetadata{Offset: tc.offset, Limit: tc.limit, Revoked: "all"}).Return(tc.svcRes, tc.svcErr)
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
var errRes respBody
|
||||
|
||||
+20
-1
@@ -79,7 +79,6 @@ func (lm *loggingMiddleware) ListSerials(ctx context.Context, clientID string, p
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("request_id", middleware.GetReqID(ctx)),
|
||||
slog.String("client_id", clientID),
|
||||
slog.String("revoke", pm.Revoked),
|
||||
slog.Group("page",
|
||||
slog.Uint64("offset", cp.Offset),
|
||||
slog.Uint64("limit", cp.Limit),
|
||||
@@ -136,3 +135,23 @@ func (lm *loggingMiddleware) RevokeCert(ctx context.Context, domainID, token, cl
|
||||
|
||||
return lm.svc.RevokeCert(ctx, domainID, token, clientID)
|
||||
}
|
||||
|
||||
// RevokeBySerial logs the revoke_by_serial request. It logs the serial ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) RevokeBySerial(ctx context.Context, serialID string) (c certs.Revoke, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("request_id", middleware.GetReqID(ctx)),
|
||||
slog.String("serial_id", serialID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.String("error", err.Error()))
|
||||
lm.logger.Warn("Revoke certificate by serial failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Revoke certificate by serial completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RevokeBySerial(ctx, serialID)
|
||||
}
|
||||
|
||||
@@ -79,3 +79,13 @@ func (ms *metricsMiddleware) RevokeCert(ctx context.Context, domainID, token, cl
|
||||
|
||||
return ms.svc.RevokeCert(ctx, domainID, token, clientID)
|
||||
}
|
||||
|
||||
// RevokeBySerial instruments RevokeBySerial method with metrics.
|
||||
func (ms *metricsMiddleware) RevokeBySerial(ctx context.Context, serialID string) (certs.Revoke, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "revoke_by_serial").Add(1)
|
||||
ms.latency.With("method", "revoke_by_serial").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.RevokeBySerial(ctx, serialID)
|
||||
}
|
||||
|
||||
+16
-4
@@ -68,13 +68,13 @@ func (req *viewReq) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type revokeReq struct {
|
||||
type revokeAllReq struct {
|
||||
token string
|
||||
certID string
|
||||
clientID string
|
||||
domainID string
|
||||
}
|
||||
|
||||
func (req *revokeReq) validate() error {
|
||||
func (req *revokeAllReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
@@ -83,7 +83,19 @@ func (req *revokeReq) validate() error {
|
||||
return apiutil.ErrMissingDomainID
|
||||
}
|
||||
|
||||
if req.certID == "" {
|
||||
if req.clientID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type revokeBySerialReq struct {
|
||||
serialID string
|
||||
}
|
||||
|
||||
func (req *revokeBySerialReq) validate() error {
|
||||
if req.serialID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ type certsRes struct {
|
||||
Key string `json:"key,omitempty"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
ExpiryTime time.Time `json:"expiry_time"`
|
||||
CAChain []string `json:"ca_chain,omitempty"`
|
||||
IssuingCA string `json:"issuing_ca,omitempty"`
|
||||
Revoked bool `json:"revoked"`
|
||||
issued bool
|
||||
}
|
||||
|
||||
+23
-10
@@ -25,8 +25,7 @@ const (
|
||||
contentType = "application/json"
|
||||
offsetKey = "offset"
|
||||
limitKey = "limit"
|
||||
revokeKey = "revoked"
|
||||
defRevoke = "false"
|
||||
revokedKey = "revoked"
|
||||
defOffset = 0
|
||||
defLimit = 10
|
||||
)
|
||||
@@ -57,12 +56,18 @@ func MakeHandler(svc certs.Service, authn smqauthn.Authentication, logger *slog.
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "view").ServeHTTP)
|
||||
r.Delete("/{certID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
revokeCert(svc),
|
||||
decodeRevokeCerts,
|
||||
r.Post("/{clientID}/revoke-all", otelhttp.NewHandler(kithttp.NewServer(
|
||||
revokeAllCerts(svc),
|
||||
decodeRevokeAllCerts,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "revoke").ServeHTTP)
|
||||
r.Post("/{certID}/revoke", otelhttp.NewHandler(kithttp.NewServer(
|
||||
revokeBySerial(svc),
|
||||
decodeRevokeBySerial,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "revoke_by_serial").ServeHTTP)
|
||||
})
|
||||
r.Get("/serials/{clientID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
listSerials(svc),
|
||||
@@ -87,7 +92,7 @@ func decodeListCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
rv, err := apiutil.ReadStringQuery(r, revokeKey, defRevoke)
|
||||
revoked, err := apiutil.ReadStringQuery(r, revokedKey, "all")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
@@ -97,7 +102,7 @@ func decodeListCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
pm: certs.PageMetadata{
|
||||
Offset: o,
|
||||
Limit: l,
|
||||
Revoked: rv,
|
||||
Revoked: revoked,
|
||||
},
|
||||
}
|
||||
return req, nil
|
||||
@@ -127,12 +132,20 @@ func decodeCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeRevokeCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := revokeReq{
|
||||
func decodeRevokeAllCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := revokeAllReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
certID: chi.URLParam(r, "certID"),
|
||||
clientID: chi.URLParam(r, "clientID"),
|
||||
domainID: chi.URLParam(r, "domainID"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeRevokeBySerial(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := revokeBySerialReq{
|
||||
serialID: chi.URLParam(r, "certID"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
+28
-3
@@ -4,6 +4,7 @@
|
||||
package certs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
@@ -15,11 +16,13 @@ import (
|
||||
|
||||
type Cert struct {
|
||||
SerialNumber string `json:"serial_number"`
|
||||
CAChain []string `json:"ca_chain,omitempty"`
|
||||
IssuingCA string `json:"issuing_ca,omitempty"`
|
||||
Certificate string `json:"certificate,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Revoked bool `json:"revoked"`
|
||||
ExpiryTime time.Time `json:"expiry_time"`
|
||||
ClientID string `json:"entity_id"`
|
||||
Revoked bool `json:"revoked"`
|
||||
}
|
||||
|
||||
type CertPage struct {
|
||||
@@ -29,12 +32,34 @@ type CertPage struct {
|
||||
Certificates []Cert `json:"certificates,omitempty"`
|
||||
}
|
||||
|
||||
// Repository specifies a Config persistence API.
|
||||
type Repository interface {
|
||||
// Save saves cert for client into database
|
||||
Save(ctx context.Context, cert Cert) (string, error)
|
||||
|
||||
// Update updates an existing certificate in the database
|
||||
Update(ctx context.Context, cert Cert) error
|
||||
|
||||
// RetrieveAll retrieve issued certificates
|
||||
RetrieveAll(ctx context.Context, offset, limit uint64) (CertPage, error)
|
||||
|
||||
// Remove removes certificate from DB for a given client ID
|
||||
Remove(ctx context.Context, clientID string) error
|
||||
|
||||
// RemoveBySerial removes certificate from DB for a given serial number
|
||||
RemoveBySerial(ctx context.Context, serialID string) error
|
||||
|
||||
// RetrieveByClient retrieves issued certificates for a given client ID
|
||||
RetrieveByClient(ctx context.Context, clientID string, pm PageMetadata) (CertPage, error)
|
||||
|
||||
// RetrieveBySerial retrieves a certificate for a given serial ID
|
||||
RetrieveBySerial(ctx context.Context, serialID string) (Cert, error)
|
||||
}
|
||||
|
||||
type PageMetadata struct {
|
||||
Total uint64 `json:"total,omitempty"`
|
||||
Offset uint64 `json:"offset,omitempty"`
|
||||
Limit uint64 `json:"limit,omitempty"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
CommonName string `json:"common_name,omitempty"`
|
||||
Revoked string `json:"revoked,omitempty"`
|
||||
}
|
||||
|
||||
+27
-28
@@ -8,8 +8,7 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"github.com/absmach/certs/sdk"
|
||||
"github.com/absmach/supermq/certs/pki/amcerts"
|
||||
"github.com/absmach/supermq/certs"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
@@ -41,22 +40,22 @@ func (_m *Agent) EXPECT() *Agent_Expecter {
|
||||
}
|
||||
|
||||
// Issue provides a mock function for the type Agent
|
||||
func (_mock *Agent) Issue(entityId string, ttl string, ipAddrs []string) (amcerts.Cert, error) {
|
||||
func (_mock *Agent) Issue(entityId string, ttl string, ipAddrs []string) (certs.Cert, error) {
|
||||
ret := _mock.Called(entityId, ttl, ipAddrs)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Issue")
|
||||
}
|
||||
|
||||
var r0 amcerts.Cert
|
||||
var r0 certs.Cert
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(string, string, []string) (amcerts.Cert, error)); ok {
|
||||
if returnFunc, ok := ret.Get(0).(func(string, string, []string) (certs.Cert, error)); ok {
|
||||
return returnFunc(entityId, ttl, ipAddrs)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(string, string, []string) amcerts.Cert); ok {
|
||||
if returnFunc, ok := ret.Get(0).(func(string, string, []string) certs.Cert); ok {
|
||||
r0 = returnFunc(entityId, ttl, ipAddrs)
|
||||
} else {
|
||||
r0 = ret.Get(0).(amcerts.Cert)
|
||||
r0 = ret.Get(0).(certs.Cert)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(string, string, []string) error); ok {
|
||||
r1 = returnFunc(entityId, ttl, ipAddrs)
|
||||
@@ -102,35 +101,35 @@ func (_c *Agent_Issue_Call) Run(run func(entityId string, ttl string, ipAddrs []
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Agent_Issue_Call) Return(cert amcerts.Cert, err error) *Agent_Issue_Call {
|
||||
func (_c *Agent_Issue_Call) Return(cert certs.Cert, err error) *Agent_Issue_Call {
|
||||
_c.Call.Return(cert, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Agent_Issue_Call) RunAndReturn(run func(entityId string, ttl string, ipAddrs []string) (amcerts.Cert, error)) *Agent_Issue_Call {
|
||||
func (_c *Agent_Issue_Call) RunAndReturn(run func(entityId string, ttl string, ipAddrs []string) (certs.Cert, error)) *Agent_Issue_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListCerts provides a mock function for the type Agent
|
||||
func (_mock *Agent) ListCerts(pm sdk.PageMetadata) (amcerts.CertPage, error) {
|
||||
func (_mock *Agent) ListCerts(pm certs.PageMetadata) (certs.CertPage, error) {
|
||||
ret := _mock.Called(pm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListCerts")
|
||||
}
|
||||
|
||||
var r0 amcerts.CertPage
|
||||
var r0 certs.CertPage
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(sdk.PageMetadata) (amcerts.CertPage, error)); ok {
|
||||
if returnFunc, ok := ret.Get(0).(func(certs.PageMetadata) (certs.CertPage, error)); ok {
|
||||
return returnFunc(pm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(sdk.PageMetadata) amcerts.CertPage); ok {
|
||||
if returnFunc, ok := ret.Get(0).(func(certs.PageMetadata) certs.CertPage); ok {
|
||||
r0 = returnFunc(pm)
|
||||
} else {
|
||||
r0 = ret.Get(0).(amcerts.CertPage)
|
||||
r0 = ret.Get(0).(certs.CertPage)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(sdk.PageMetadata) error); ok {
|
||||
if returnFunc, ok := ret.Get(1).(func(certs.PageMetadata) error); ok {
|
||||
r1 = returnFunc(pm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
@@ -144,16 +143,16 @@ type Agent_ListCerts_Call struct {
|
||||
}
|
||||
|
||||
// ListCerts is a helper method to define mock.On call
|
||||
// - pm sdk.PageMetadata
|
||||
// - pm certs.PageMetadata
|
||||
func (_e *Agent_Expecter) ListCerts(pm interface{}) *Agent_ListCerts_Call {
|
||||
return &Agent_ListCerts_Call{Call: _e.mock.On("ListCerts", pm)}
|
||||
}
|
||||
|
||||
func (_c *Agent_ListCerts_Call) Run(run func(pm sdk.PageMetadata)) *Agent_ListCerts_Call {
|
||||
func (_c *Agent_ListCerts_Call) Run(run func(pm certs.PageMetadata)) *Agent_ListCerts_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 sdk.PageMetadata
|
||||
var arg0 certs.PageMetadata
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(sdk.PageMetadata)
|
||||
arg0 = args[0].(certs.PageMetadata)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
@@ -162,12 +161,12 @@ func (_c *Agent_ListCerts_Call) Run(run func(pm sdk.PageMetadata)) *Agent_ListCe
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Agent_ListCerts_Call) Return(certPage amcerts.CertPage, err error) *Agent_ListCerts_Call {
|
||||
func (_c *Agent_ListCerts_Call) Return(certPage certs.CertPage, err error) *Agent_ListCerts_Call {
|
||||
_c.Call.Return(certPage, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Agent_ListCerts_Call) RunAndReturn(run func(pm sdk.PageMetadata) (amcerts.CertPage, error)) *Agent_ListCerts_Call {
|
||||
func (_c *Agent_ListCerts_Call) RunAndReturn(run func(pm certs.PageMetadata) (certs.CertPage, error)) *Agent_ListCerts_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -224,22 +223,22 @@ func (_c *Agent_Revoke_Call) RunAndReturn(run func(serialNumber string) error) *
|
||||
}
|
||||
|
||||
// View provides a mock function for the type Agent
|
||||
func (_mock *Agent) View(serialNumber string) (amcerts.Cert, error) {
|
||||
func (_mock *Agent) View(serialNumber string) (certs.Cert, error) {
|
||||
ret := _mock.Called(serialNumber)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for View")
|
||||
}
|
||||
|
||||
var r0 amcerts.Cert
|
||||
var r0 certs.Cert
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(string) (amcerts.Cert, error)); ok {
|
||||
if returnFunc, ok := ret.Get(0).(func(string) (certs.Cert, error)); ok {
|
||||
return returnFunc(serialNumber)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(string) amcerts.Cert); ok {
|
||||
if returnFunc, ok := ret.Get(0).(func(string) certs.Cert); ok {
|
||||
r0 = returnFunc(serialNumber)
|
||||
} else {
|
||||
r0 = ret.Get(0).(amcerts.Cert)
|
||||
r0 = ret.Get(0).(certs.Cert)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = returnFunc(serialNumber)
|
||||
@@ -273,12 +272,12 @@ func (_c *Agent_View_Call) Run(run func(serialNumber string)) *Agent_View_Call {
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Agent_View_Call) Return(cert amcerts.Cert, err error) *Agent_View_Call {
|
||||
func (_c *Agent_View_Call) Return(cert certs.Cert, err error) *Agent_View_Call {
|
||||
_c.Call.Return(cert, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Agent_View_Call) RunAndReturn(run func(serialNumber string) (amcerts.Cert, error)) *Agent_View_Call {
|
||||
func (_c *Agent_View_Call) RunAndReturn(run func(serialNumber string) (certs.Cert, error)) *Agent_View_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/certs"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// NewRepository creates a new instance of Repository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewRepository(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Repository {
|
||||
mock := &Repository{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// Repository is an autogenerated mock type for the Repository type
|
||||
type Repository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type Repository_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *Repository) EXPECT() *Repository_Expecter {
|
||||
return &Repository_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Remove provides a mock function for the type Repository
|
||||
func (_mock *Repository) Remove(ctx context.Context, clientID string) error {
|
||||
ret := _mock.Called(ctx, clientID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Remove")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||
r0 = returnFunc(ctx, clientID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Repository_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove'
|
||||
type Repository_Remove_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Remove is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - clientID string
|
||||
func (_e *Repository_Expecter) Remove(ctx interface{}, clientID interface{}) *Repository_Remove_Call {
|
||||
return &Repository_Remove_Call{Call: _e.mock.On("Remove", ctx, clientID)}
|
||||
}
|
||||
|
||||
func (_c *Repository_Remove_Call) Run(run func(ctx context.Context, clientID string)) *Repository_Remove_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_Remove_Call) Return(err error) *Repository_Remove_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_Remove_Call) RunAndReturn(run func(ctx context.Context, clientID string) error) *Repository_Remove_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RemoveBySerial provides a mock function for the type Repository
|
||||
func (_mock *Repository) RemoveBySerial(ctx context.Context, serialID string) error {
|
||||
ret := _mock.Called(ctx, serialID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RemoveBySerial")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||
r0 = returnFunc(ctx, serialID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Repository_RemoveBySerial_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveBySerial'
|
||||
type Repository_RemoveBySerial_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RemoveBySerial is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - serialID string
|
||||
func (_e *Repository_Expecter) RemoveBySerial(ctx interface{}, serialID interface{}) *Repository_RemoveBySerial_Call {
|
||||
return &Repository_RemoveBySerial_Call{Call: _e.mock.On("RemoveBySerial", ctx, serialID)}
|
||||
}
|
||||
|
||||
func (_c *Repository_RemoveBySerial_Call) Run(run func(ctx context.Context, serialID string)) *Repository_RemoveBySerial_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_RemoveBySerial_Call) Return(err error) *Repository_RemoveBySerial_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_RemoveBySerial_Call) RunAndReturn(run func(ctx context.Context, serialID string) error) *Repository_RemoveBySerial_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RetrieveAll provides a mock function for the type Repository
|
||||
func (_mock *Repository) RetrieveAll(ctx context.Context, offset uint64, limit uint64) (certs.CertPage, error) {
|
||||
ret := _mock.Called(ctx, offset, limit)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RetrieveAll")
|
||||
}
|
||||
|
||||
var r0 certs.CertPage
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, uint64, uint64) (certs.CertPage, error)); ok {
|
||||
return returnFunc(ctx, offset, limit)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, uint64, uint64) certs.CertPage); ok {
|
||||
r0 = returnFunc(ctx, offset, limit)
|
||||
} else {
|
||||
r0 = ret.Get(0).(certs.CertPage)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, uint64, uint64) error); ok {
|
||||
r1 = returnFunc(ctx, offset, limit)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Repository_RetrieveAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveAll'
|
||||
type Repository_RetrieveAll_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RetrieveAll is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - offset uint64
|
||||
// - limit uint64
|
||||
func (_e *Repository_Expecter) RetrieveAll(ctx interface{}, offset interface{}, limit interface{}) *Repository_RetrieveAll_Call {
|
||||
return &Repository_RetrieveAll_Call{Call: _e.mock.On("RetrieveAll", ctx, offset, limit)}
|
||||
}
|
||||
|
||||
func (_c *Repository_RetrieveAll_Call) Run(run func(ctx context.Context, offset uint64, limit uint64)) *Repository_RetrieveAll_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 uint64
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(uint64)
|
||||
}
|
||||
var arg2 uint64
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(uint64)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_RetrieveAll_Call) Return(certPage certs.CertPage, err error) *Repository_RetrieveAll_Call {
|
||||
_c.Call.Return(certPage, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_RetrieveAll_Call) RunAndReturn(run func(ctx context.Context, offset uint64, limit uint64) (certs.CertPage, error)) *Repository_RetrieveAll_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RetrieveByClient provides a mock function for the type Repository
|
||||
func (_mock *Repository) RetrieveByClient(ctx context.Context, clientID string, pm certs.PageMetadata) (certs.CertPage, error) {
|
||||
ret := _mock.Called(ctx, clientID, pm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RetrieveByClient")
|
||||
}
|
||||
|
||||
var r0 certs.CertPage
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, certs.PageMetadata) (certs.CertPage, error)); ok {
|
||||
return returnFunc(ctx, clientID, pm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, certs.PageMetadata) certs.CertPage); ok {
|
||||
r0 = returnFunc(ctx, clientID, pm)
|
||||
} else {
|
||||
r0 = ret.Get(0).(certs.CertPage)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, certs.PageMetadata) error); ok {
|
||||
r1 = returnFunc(ctx, clientID, pm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Repository_RetrieveByClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveByClient'
|
||||
type Repository_RetrieveByClient_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RetrieveByClient is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - clientID string
|
||||
// - pm certs.PageMetadata
|
||||
func (_e *Repository_Expecter) RetrieveByClient(ctx interface{}, clientID interface{}, pm interface{}) *Repository_RetrieveByClient_Call {
|
||||
return &Repository_RetrieveByClient_Call{Call: _e.mock.On("RetrieveByClient", ctx, clientID, pm)}
|
||||
}
|
||||
|
||||
func (_c *Repository_RetrieveByClient_Call) Run(run func(ctx context.Context, clientID string, pm certs.PageMetadata)) *Repository_RetrieveByClient_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 certs.PageMetadata
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(certs.PageMetadata)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_RetrieveByClient_Call) Return(certPage certs.CertPage, err error) *Repository_RetrieveByClient_Call {
|
||||
_c.Call.Return(certPage, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_RetrieveByClient_Call) RunAndReturn(run func(ctx context.Context, clientID string, pm certs.PageMetadata) (certs.CertPage, error)) *Repository_RetrieveByClient_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RetrieveBySerial provides a mock function for the type Repository
|
||||
func (_mock *Repository) RetrieveBySerial(ctx context.Context, serialID string) (certs.Cert, error) {
|
||||
ret := _mock.Called(ctx, serialID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RetrieveBySerial")
|
||||
}
|
||||
|
||||
var r0 certs.Cert
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) (certs.Cert, error)); ok {
|
||||
return returnFunc(ctx, serialID)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) certs.Cert); ok {
|
||||
r0 = returnFunc(ctx, serialID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(certs.Cert)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = returnFunc(ctx, serialID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Repository_RetrieveBySerial_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveBySerial'
|
||||
type Repository_RetrieveBySerial_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RetrieveBySerial is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - serialID string
|
||||
func (_e *Repository_Expecter) RetrieveBySerial(ctx interface{}, serialID interface{}) *Repository_RetrieveBySerial_Call {
|
||||
return &Repository_RetrieveBySerial_Call{Call: _e.mock.On("RetrieveBySerial", ctx, serialID)}
|
||||
}
|
||||
|
||||
func (_c *Repository_RetrieveBySerial_Call) Run(run func(ctx context.Context, serialID string)) *Repository_RetrieveBySerial_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_RetrieveBySerial_Call) Return(cert certs.Cert, err error) *Repository_RetrieveBySerial_Call {
|
||||
_c.Call.Return(cert, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_RetrieveBySerial_Call) RunAndReturn(run func(ctx context.Context, serialID string) (certs.Cert, error)) *Repository_RetrieveBySerial_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Save provides a mock function for the type Repository
|
||||
func (_mock *Repository) Save(ctx context.Context, cert certs.Cert) (string, error) {
|
||||
ret := _mock.Called(ctx, cert)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Save")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, certs.Cert) (string, error)); ok {
|
||||
return returnFunc(ctx, cert)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, certs.Cert) string); ok {
|
||||
r0 = returnFunc(ctx, cert)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, certs.Cert) error); ok {
|
||||
r1 = returnFunc(ctx, cert)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Repository_Save_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Save'
|
||||
type Repository_Save_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Save is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - cert certs.Cert
|
||||
func (_e *Repository_Expecter) Save(ctx interface{}, cert interface{}) *Repository_Save_Call {
|
||||
return &Repository_Save_Call{Call: _e.mock.On("Save", ctx, cert)}
|
||||
}
|
||||
|
||||
func (_c *Repository_Save_Call) Run(run func(ctx context.Context, cert certs.Cert)) *Repository_Save_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 certs.Cert
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(certs.Cert)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_Save_Call) Return(s string, err error) *Repository_Save_Call {
|
||||
_c.Call.Return(s, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_Save_Call) RunAndReturn(run func(ctx context.Context, cert certs.Cert) (string, error)) *Repository_Save_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Update provides a mock function for the type Repository
|
||||
func (_mock *Repository) Update(ctx context.Context, cert certs.Cert) error {
|
||||
ret := _mock.Called(ctx, cert)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Update")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, certs.Cert) error); ok {
|
||||
r0 = returnFunc(ctx, cert)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Repository_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update'
|
||||
type Repository_Update_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Update is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - cert certs.Cert
|
||||
func (_e *Repository_Expecter) Update(ctx interface{}, cert interface{}) *Repository_Update_Call {
|
||||
return &Repository_Update_Call{Call: _e.mock.On("Update", ctx, cert)}
|
||||
}
|
||||
|
||||
func (_c *Repository_Update_Call) Run(run func(ctx context.Context, cert certs.Cert)) *Repository_Update_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 certs.Cert
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(certs.Cert)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_Update_Call) Return(err error) *Repository_Update_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_Update_Call) RunAndReturn(run func(ctx context.Context, cert certs.Cert) error) *Repository_Update_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -269,6 +269,72 @@ func (_c *Service_ListSerials_Call) RunAndReturn(run func(ctx context.Context, c
|
||||
return _c
|
||||
}
|
||||
|
||||
// RevokeBySerial provides a mock function for the type Service
|
||||
func (_mock *Service) RevokeBySerial(ctx context.Context, serialID string) (certs.Revoke, error) {
|
||||
ret := _mock.Called(ctx, serialID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RevokeBySerial")
|
||||
}
|
||||
|
||||
var r0 certs.Revoke
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) (certs.Revoke, error)); ok {
|
||||
return returnFunc(ctx, serialID)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) certs.Revoke); ok {
|
||||
r0 = returnFunc(ctx, serialID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(certs.Revoke)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = returnFunc(ctx, serialID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Service_RevokeBySerial_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RevokeBySerial'
|
||||
type Service_RevokeBySerial_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RevokeBySerial is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - serialID string
|
||||
func (_e *Service_Expecter) RevokeBySerial(ctx interface{}, serialID interface{}) *Service_RevokeBySerial_Call {
|
||||
return &Service_RevokeBySerial_Call{Call: _e.mock.On("RevokeBySerial", ctx, serialID)}
|
||||
}
|
||||
|
||||
func (_c *Service_RevokeBySerial_Call) Run(run func(ctx context.Context, serialID string)) *Service_RevokeBySerial_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_RevokeBySerial_Call) Return(revoke certs.Revoke, err error) *Service_RevokeBySerial_Call {
|
||||
_c.Call.Return(revoke, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_RevokeBySerial_Call) RunAndReturn(run func(ctx context.Context, serialID string) (certs.Revoke, error)) *Service_RevokeBySerial_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RevokeCert provides a mock function for the type Service
|
||||
func (_mock *Service) RevokeCert(ctx context.Context, domainID string, token string, clientID string) (certs.Revoke, error) {
|
||||
ret := _mock.Called(ctx, domainID, token, clientID)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package certs
|
||||
|
||||
// Agent represents the PKI interface that all PKI implementations must satisfy.
|
||||
type Agent interface {
|
||||
Issue(entityId, ttl string, ipAddrs []string) (Cert, error)
|
||||
View(serialNumber string) (Cert, error)
|
||||
Revoke(serialNumber string) error
|
||||
ListCerts(pm PageMetadata) (CertPage, error)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package openbao
|
||||
@@ -0,0 +1,356 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package openbao wraps OpenBao client for PKI operations
|
||||
package openbao
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/certs"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/openbao/openbao/api/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
issue = "issue"
|
||||
cert = "cert"
|
||||
revoke = "revoke"
|
||||
)
|
||||
|
||||
var (
|
||||
errFailedToLogin = errors.New("failed to login to OpenBao")
|
||||
errNoAuthInfo = errors.New("no auth information from OpenBao")
|
||||
errRenewWatcher = errors.New("unable to initialize new lifetime watcher for renewing auth token")
|
||||
)
|
||||
|
||||
// Agent represents the OpenBao PKI interface.
|
||||
type Agent interface {
|
||||
Issue(entityId, ttl string, ipAddrs []string) (certs.Cert, error)
|
||||
View(serialNumber string) (certs.Cert, error)
|
||||
Revoke(serialNumber string) error
|
||||
ListCerts(pm certs.PageMetadata) (certs.CertPage, error)
|
||||
}
|
||||
|
||||
type openbaoPKIAgent struct {
|
||||
appRole string
|
||||
appSecret string
|
||||
namespace string
|
||||
path string
|
||||
role string
|
||||
host string
|
||||
issueURL string
|
||||
readURL string
|
||||
revokeURL string
|
||||
client *api.Client
|
||||
secret *api.Secret
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewAgent instantiates an OpenBao PKI client.
|
||||
func NewAgent(appRole, appSecret, host, namespace, path, role string, logger *slog.Logger) (Agent, error) {
|
||||
conf := api.DefaultConfig()
|
||||
conf.Address = host
|
||||
|
||||
client, err := api.NewClient(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if namespace != "" {
|
||||
client.SetNamespace(namespace)
|
||||
}
|
||||
|
||||
p := openbaoPKIAgent{
|
||||
appRole: appRole,
|
||||
appSecret: appSecret,
|
||||
host: host,
|
||||
namespace: namespace,
|
||||
role: role,
|
||||
path: path,
|
||||
client: client,
|
||||
logger: logger,
|
||||
issueURL: "/" + path + "/" + issue + "/" + role,
|
||||
readURL: "/" + path + "/" + cert + "/",
|
||||
revokeURL: "/" + path + "/" + revoke,
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (va *openbaoPKIAgent) Issue(entityId, ttl string, ipAddrs []string) (certs.Cert, error) {
|
||||
err := va.LoginAndRenew()
|
||||
if err != nil {
|
||||
return certs.Cert{}, err
|
||||
}
|
||||
|
||||
secretValues := map[string]interface{}{
|
||||
"common_name": entityId,
|
||||
"ttl": ttl,
|
||||
"exclude_cn_from_sans": true,
|
||||
}
|
||||
|
||||
if len(ipAddrs) > 0 {
|
||||
secretValues["ip_sans"] = ipAddrs
|
||||
}
|
||||
|
||||
secret, err := va.client.Logical().Write(va.issueURL, secretValues)
|
||||
if err != nil {
|
||||
return certs.Cert{}, err
|
||||
}
|
||||
|
||||
if secret == nil || secret.Data == nil {
|
||||
return certs.Cert{}, fmt.Errorf("no certificate data returned from OpenBao")
|
||||
}
|
||||
|
||||
cert := certs.Cert{
|
||||
ClientID: entityId,
|
||||
}
|
||||
|
||||
if certData, ok := secret.Data["certificate"].(string); ok {
|
||||
cert.Certificate = certData
|
||||
}
|
||||
|
||||
if keyData, ok := secret.Data["private_key"].(string); ok {
|
||||
cert.Key = keyData
|
||||
}
|
||||
|
||||
if serialNumber, ok := secret.Data["serial_number"].(string); ok {
|
||||
cert.SerialNumber = serialNumber
|
||||
}
|
||||
if caChain, ok := secret.Data["ca_chain"].([]interface{}); ok {
|
||||
for _, ca := range caChain {
|
||||
if caStr, ok := ca.(string); ok {
|
||||
cert.CAChain = append(cert.CAChain, caStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
if issuingCA, ok := secret.Data["issuing_ca"].(string); ok {
|
||||
cert.IssuingCA = issuingCA
|
||||
}
|
||||
|
||||
if expirationInterface, ok := secret.Data["expiration"]; ok {
|
||||
switch exp := expirationInterface.(type) {
|
||||
case int64:
|
||||
cert.ExpiryTime = time.Unix(exp, 0)
|
||||
case float64:
|
||||
cert.ExpiryTime = time.Unix(int64(exp), 0)
|
||||
case json.Number:
|
||||
if expInt, err := exp.Int64(); err == nil {
|
||||
cert.ExpiryTime = time.Unix(expInt, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (va *openbaoPKIAgent) View(serialNumber string) (certs.Cert, error) {
|
||||
err := va.LoginAndRenew()
|
||||
if err != nil {
|
||||
return certs.Cert{}, err
|
||||
}
|
||||
|
||||
secret, err := va.client.Logical().Read(va.readURL + serialNumber)
|
||||
if err != nil {
|
||||
return certs.Cert{}, err
|
||||
}
|
||||
|
||||
if secret == nil || secret.Data == nil {
|
||||
return certs.Cert{}, fmt.Errorf("certificate not found")
|
||||
}
|
||||
|
||||
cert := certs.Cert{
|
||||
SerialNumber: serialNumber,
|
||||
}
|
||||
|
||||
if certData, ok := secret.Data["certificate"].(string); ok {
|
||||
cert.Certificate = certData
|
||||
}
|
||||
|
||||
if cert.Certificate != "" {
|
||||
if expiry, err := va.parseCertificateExpiry(cert.Certificate); err == nil {
|
||||
cert.ExpiryTime = expiry
|
||||
}
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (va *openbaoPKIAgent) parseCertificateExpiry(certPEM string) (time.Time, error) {
|
||||
block, _ := pem.Decode([]byte(certPEM))
|
||||
if block == nil {
|
||||
return time.Time{}, fmt.Errorf("failed to decode PEM certificate")
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("failed to parse X509 certificate: %w", err)
|
||||
}
|
||||
|
||||
return cert.NotAfter, nil
|
||||
}
|
||||
|
||||
func (va *openbaoPKIAgent) Revoke(serialNumber string) error {
|
||||
err := va.LoginAndRenew()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
secretValues := map[string]interface{}{
|
||||
"serial_number": serialNumber,
|
||||
}
|
||||
|
||||
_, err = va.client.Logical().Write(va.revokeURL, secretValues)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (va *openbaoPKIAgent) ListCerts(pm certs.PageMetadata) (certs.CertPage, error) {
|
||||
err := va.LoginAndRenew()
|
||||
if err != nil {
|
||||
return certs.CertPage{}, err
|
||||
}
|
||||
|
||||
secret, err := va.client.Logical().List(va.path + "/certs")
|
||||
if err != nil {
|
||||
return certs.CertPage{}, err
|
||||
}
|
||||
|
||||
certPage := certs.CertPage{
|
||||
Certificates: []certs.Cert{},
|
||||
Limit: pm.Limit,
|
||||
Offset: pm.Offset,
|
||||
}
|
||||
|
||||
if secret == nil || secret.Data == nil {
|
||||
return certPage, nil
|
||||
}
|
||||
|
||||
keysInterface, ok := secret.Data["keys"]
|
||||
if !ok {
|
||||
return certPage, nil
|
||||
}
|
||||
|
||||
var serialNumbers []string
|
||||
if err := mapstructure.Decode(keysInterface, &serialNumbers); err != nil {
|
||||
return certPage, fmt.Errorf("failed to decode certificate serial numbers: %w", err)
|
||||
}
|
||||
|
||||
var filteredCerts []certs.Cert
|
||||
for _, serialNumber := range serialNumbers {
|
||||
cert, err := va.View(serialNumber)
|
||||
if err != nil {
|
||||
va.logger.Warn("failed to retrieve certificate details", "serial", serialNumber, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if pm.CommonName != "" {
|
||||
if !va.matchesCommonName(cert.Certificate, pm.CommonName) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
filteredCerts = append(filteredCerts, cert)
|
||||
}
|
||||
|
||||
certPage.Total = uint64(len(filteredCerts))
|
||||
|
||||
start := pm.Offset
|
||||
end := pm.Offset + pm.Limit
|
||||
if pm.Limit == 0 {
|
||||
end = uint64(len(filteredCerts))
|
||||
}
|
||||
if start >= uint64(len(filteredCerts)) {
|
||||
return certPage, nil
|
||||
}
|
||||
if end > uint64(len(filteredCerts)) {
|
||||
end = uint64(len(filteredCerts))
|
||||
}
|
||||
|
||||
for i := start; i < end; i++ {
|
||||
certPage.Certificates = append(certPage.Certificates, filteredCerts[i])
|
||||
}
|
||||
|
||||
return certPage, nil
|
||||
}
|
||||
|
||||
func (va *openbaoPKIAgent) LoginAndRenew() error {
|
||||
if va.secret != nil && va.secret.Auth != nil && va.secret.Auth.ClientToken != "" {
|
||||
_, err := va.client.Auth().Token().LookupSelf()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
authData := map[string]interface{}{
|
||||
"role_id": va.appRole,
|
||||
"secret_id": va.appSecret,
|
||||
}
|
||||
|
||||
authResp, err := va.client.Logical().Write("auth/approle/login", authData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", errFailedToLogin, err)
|
||||
}
|
||||
|
||||
if authResp == nil || authResp.Auth == nil {
|
||||
return errNoAuthInfo
|
||||
}
|
||||
|
||||
va.secret = authResp
|
||||
va.client.SetToken(authResp.Auth.ClientToken)
|
||||
|
||||
if authResp.Auth.Renewable {
|
||||
watcher, err := va.client.NewLifetimeWatcher(&api.LifetimeWatcherInput{
|
||||
Secret: authResp,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", errRenewWatcher, err)
|
||||
}
|
||||
|
||||
go va.renewToken(watcher)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (va *openbaoPKIAgent) renewToken(watcher *api.LifetimeWatcher) {
|
||||
defer watcher.Stop()
|
||||
|
||||
watcher.Start()
|
||||
for {
|
||||
select {
|
||||
case err := <-watcher.DoneCh():
|
||||
if err != nil {
|
||||
va.logger.Error("token renewal failed", "error", err)
|
||||
}
|
||||
return
|
||||
case renewal := <-watcher.RenewCh():
|
||||
va.logger.Info("token renewed successfully", "lease_duration", renewal.Secret.LeaseDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (va *openbaoPKIAgent) matchesCommonName(certPEM, expectedCommonName string) bool {
|
||||
if certPEM == "" || expectedCommonName == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
block, _ := pem.Decode([]byte(certPEM))
|
||||
if block == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return cert.Subject.CommonName == expectedCommonName
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package pki contains the domain concept definitions needed to
|
||||
// support SuperMQ Certs service functionality.
|
||||
// It provides the abstraction of the PKI (Public Key Infrastructure)
|
||||
// Valut service, which is used to issue and revoke certificates.
|
||||
package pki
|
||||
@@ -1,269 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package pki wraps vault client
|
||||
package pki
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/api/auth/approle"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
const (
|
||||
issue = "issue"
|
||||
cert = "cert"
|
||||
revoke = "revoke"
|
||||
)
|
||||
|
||||
var (
|
||||
errFailedCertDecoding = errors.New("failed to decode response from vault service")
|
||||
errFailedToLogin = errors.New("failed to login to Vault")
|
||||
errFailedAppRole = errors.New("failed to create vault new app role")
|
||||
errNoAuthInfo = errors.New("no auth information from Vault")
|
||||
errNonRenewal = errors.New("token is not configured to be renewable")
|
||||
errRenewWatcher = errors.New("unable to initialize new lifetime watcher for renewing auth token")
|
||||
errFailedRenew = errors.New("failed to renew token")
|
||||
errCouldNotRenew = errors.New("token can no longer be renewed")
|
||||
)
|
||||
|
||||
type Cert struct {
|
||||
ClientCert string `json:"client_cert" mapstructure:"certificate"`
|
||||
IssuingCA string `json:"issuing_ca" mapstructure:"issuing_ca"`
|
||||
CAChain []string `json:"ca_chain" mapstructure:"ca_chain"`
|
||||
ClientKey string `json:"client_key" mapstructure:"private_key"`
|
||||
PrivateKeyType string `json:"private_key_type" mapstructure:"private_key_type"`
|
||||
Serial string `json:"serial" mapstructure:"serial_number"`
|
||||
Expire int64 `json:"expire" mapstructure:"expiration"`
|
||||
}
|
||||
|
||||
// Agent represents the Vault PKI interface.
|
||||
type Agent interface {
|
||||
// IssueCert issues certificate on PKI
|
||||
IssueCert(cn, ttl string) (Cert, error)
|
||||
|
||||
// Read retrieves certificate from PKI
|
||||
Read(serial string) (Cert, error)
|
||||
|
||||
// Revoke revokes certificate from PKI
|
||||
Revoke(serial string) (time.Time, error)
|
||||
|
||||
// Login to PKI and renews token
|
||||
LoginAndRenew(ctx context.Context) error
|
||||
}
|
||||
|
||||
type pkiAgent struct {
|
||||
appRole string
|
||||
appSecret string
|
||||
namespace string
|
||||
path string
|
||||
role string
|
||||
host string
|
||||
issueURL string
|
||||
readURL string
|
||||
revokeURL string
|
||||
client *api.Client
|
||||
secret *api.Secret
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
type certReq struct {
|
||||
CommonName string `json:"common_name"`
|
||||
TTL string `json:"ttl"`
|
||||
}
|
||||
|
||||
type certRevokeReq struct {
|
||||
SerialNumber string `json:"serial_number"`
|
||||
}
|
||||
|
||||
// NewVaultClient instantiates a Vault client.
|
||||
func NewVaultClient(appRole, appSecret, host, namespace, path, role string, logger *slog.Logger) (Agent, error) {
|
||||
conf := api.DefaultConfig()
|
||||
conf.Address = host
|
||||
|
||||
client, err := api.NewClient(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if namespace != "" {
|
||||
client.SetNamespace(namespace)
|
||||
}
|
||||
|
||||
p := pkiAgent{
|
||||
appRole: appRole,
|
||||
appSecret: appSecret,
|
||||
host: host,
|
||||
namespace: namespace,
|
||||
role: role,
|
||||
path: path,
|
||||
client: client,
|
||||
logger: logger,
|
||||
issueURL: "/" + path + "/" + issue + "/" + role,
|
||||
readURL: "/" + path + "/" + cert + "/",
|
||||
revokeURL: "/" + path + "/" + revoke,
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (p *pkiAgent) IssueCert(cn, ttl string) (Cert, error) {
|
||||
cReq := certReq{
|
||||
CommonName: cn,
|
||||
TTL: ttl,
|
||||
}
|
||||
|
||||
var certIssueReq map[string]interface{}
|
||||
data, err := json.Marshal(cReq)
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
if err := json.Unmarshal(data, &certIssueReq); err != nil {
|
||||
return Cert{}, nil
|
||||
}
|
||||
|
||||
s, err := p.client.Logical().Write(p.issueURL, certIssueReq)
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
|
||||
cert := Cert{}
|
||||
if err = mapstructure.Decode(s.Data, &cert); err != nil {
|
||||
return Cert{}, errors.Wrap(errFailedCertDecoding, err)
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (p *pkiAgent) Read(serial string) (Cert, error) {
|
||||
s, err := p.client.Logical().Read(p.readURL + serial)
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
cert := Cert{}
|
||||
if err = mapstructure.Decode(s.Data, &cert); err != nil {
|
||||
return Cert{}, errors.Wrap(errFailedCertDecoding, err)
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (p *pkiAgent) Revoke(serial string) (time.Time, error) {
|
||||
cReq := certRevokeReq{
|
||||
SerialNumber: serial,
|
||||
}
|
||||
|
||||
var certRevokeReq map[string]interface{}
|
||||
data, err := json.Marshal(cReq)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
if err := json.Unmarshal(data, &certRevokeReq); err != nil {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
|
||||
s, err := p.client.Logical().Write(p.revokeURL, certRevokeReq)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
// Vault will return a response without errors but with a warning if the certificate is expired.
|
||||
// The response will not have "revocation_time" in such cases.
|
||||
if revokeTime, ok := s.Data["revocation_time"]; ok {
|
||||
switch v := revokeTime.(type) {
|
||||
case json.Number:
|
||||
rev, err := v.Float64()
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return time.Unix(0, int64(rev)*int64(time.Second)), nil
|
||||
|
||||
default:
|
||||
return time.Time{}, fmt.Errorf("unsupported type for revocation_time: %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}, nil
|
||||
}
|
||||
|
||||
func (p *pkiAgent) LoginAndRenew(ctx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
p.logger.Info("pki login and renew function stopping")
|
||||
return nil
|
||||
default:
|
||||
err := p.login(ctx)
|
||||
if err != nil {
|
||||
p.logger.Info("unable to authenticate to Vault", slog.Any("error", err))
|
||||
time.Sleep(5 * time.Second)
|
||||
break
|
||||
}
|
||||
tokenErr := p.manageTokenLifecycle()
|
||||
if tokenErr != nil {
|
||||
p.logger.Info("unable to start managing token lifecycle", slog.Any("error", tokenErr))
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pkiAgent) login(ctx context.Context) error {
|
||||
secretID := &approle.SecretID{FromString: p.appSecret}
|
||||
|
||||
authMethod, err := approle.NewAppRoleAuth(
|
||||
p.appRole,
|
||||
secretID,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(errFailedAppRole, err)
|
||||
}
|
||||
if p.namespace != "" {
|
||||
p.client.SetNamespace(p.namespace)
|
||||
}
|
||||
secret, err := p.client.Auth().Login(ctx, authMethod)
|
||||
if err != nil {
|
||||
return errors.Wrap(errFailedToLogin, err)
|
||||
}
|
||||
if secret == nil {
|
||||
return errNoAuthInfo
|
||||
}
|
||||
p.secret = secret
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *pkiAgent) manageTokenLifecycle() error {
|
||||
renew := p.secret.Auth.Renewable
|
||||
if !renew {
|
||||
return errNonRenewal
|
||||
}
|
||||
|
||||
watcher, err := p.client.NewLifetimeWatcher(&api.LifetimeWatcherInput{
|
||||
Secret: p.secret,
|
||||
Increment: 3600, // Requesting token for 3600s = 1h, If this is more than token_max_ttl, then response token will have token_max_ttl
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(errRenewWatcher, err)
|
||||
}
|
||||
|
||||
go watcher.Start()
|
||||
defer watcher.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-watcher.DoneCh():
|
||||
if err != nil {
|
||||
return errors.Wrap(errFailedRenew, err)
|
||||
}
|
||||
// This occurs once the token has reached max TTL or if token is disabled for renewal.
|
||||
return errCouldNotRenew
|
||||
|
||||
case renewal := <-watcher.RenewCh():
|
||||
p.logger.Info("Successfully renewed token", slog.Any("renewed_at", renewal.RenewedAt))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/certs"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
"github.com/absmach/supermq/pkg/postgres"
|
||||
)
|
||||
|
||||
var _ certs.Repository = (*certsRepository)(nil)
|
||||
|
||||
type PageMetadata struct {
|
||||
Offset uint64 `db:"offset,omitempty"`
|
||||
Limit uint64 `db:"limit,omitempty"`
|
||||
ClientID string `db:"client_id,omitempty"`
|
||||
}
|
||||
|
||||
type certsRepository struct {
|
||||
db postgres.Database
|
||||
}
|
||||
|
||||
// NewRepository instantiates a PostgreSQL implementation of certs
|
||||
// repository.
|
||||
func NewRepository(db postgres.Database) certs.Repository {
|
||||
return &certsRepository{db: db}
|
||||
}
|
||||
|
||||
func (cr certsRepository) RetrieveAll(ctx context.Context, offset, limit uint64) (certs.CertPage, error) {
|
||||
pm := certs.PageMetadata{
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
}
|
||||
|
||||
return cr.retrieveCertificates(ctx, "", pm)
|
||||
}
|
||||
|
||||
func (cr certsRepository) RetrieveByClient(ctx context.Context, clientID string, pm certs.PageMetadata) (certs.CertPage, error) {
|
||||
return cr.retrieveCertificates(ctx, clientID, pm)
|
||||
}
|
||||
|
||||
func (cr certsRepository) Save(ctx context.Context, cert certs.Cert) (string, error) {
|
||||
dbcrt := toDBCert(cert)
|
||||
|
||||
q := `INSERT INTO certs (client_id, serial_number, expiry_time, revoked)
|
||||
VALUES (:client_id, :serial_number, :expiry_time, :revoked)
|
||||
RETURNING serial_number`
|
||||
|
||||
row, err := cr.db.NamedQueryContext(ctx, q, dbcrt)
|
||||
if err != nil {
|
||||
return "", postgres.HandleError(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
var serialNumber string
|
||||
if row.Next() {
|
||||
if err := row.Scan(&serialNumber); err != nil {
|
||||
return "", errors.Wrap(repoerr.ErrFailedOpDB, err)
|
||||
}
|
||||
}
|
||||
|
||||
return serialNumber, nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) Update(ctx context.Context, cert certs.Cert) error {
|
||||
dbcrt := toDBCert(cert)
|
||||
|
||||
q := `UPDATE certs SET
|
||||
client_id = :client_id,
|
||||
expiry_time = :expiry_time,
|
||||
revoked = :revoked
|
||||
WHERE serial_number = :serial_number`
|
||||
|
||||
result, err := cr.db.NamedExecContext(ctx, q, dbcrt)
|
||||
if err != nil {
|
||||
return postgres.HandleError(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrFailedOpDB, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return errors.Wrap(repoerr.ErrNotFound, errors.New("certificate not found"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) Remove(ctx context.Context, clientID string) error {
|
||||
q := `DELETE FROM certs WHERE client_id = :client_id`
|
||||
var c certs.Cert
|
||||
c.ClientID = clientID
|
||||
dbcrt := toDBCert(c)
|
||||
if _, err := cr.db.NamedExecContext(ctx, q, dbcrt); err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) RemoveBySerial(ctx context.Context, serialID string) error {
|
||||
q := `DELETE FROM certs WHERE serial_number = :serial_number`
|
||||
var c certs.Cert
|
||||
c.SerialNumber = serialID
|
||||
dbcrt := toDBCert(c)
|
||||
if _, err := cr.db.NamedExecContext(ctx, q, dbcrt); err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func PageQuery(pm certs.PageMetadata) (string, error) {
|
||||
var query []string
|
||||
|
||||
if pm.Revoked != "all" {
|
||||
switch pm.Revoked {
|
||||
case "true":
|
||||
query = append(query, "revoked = true")
|
||||
case "false":
|
||||
query = append(query, "revoked = false")
|
||||
}
|
||||
}
|
||||
|
||||
if pm.CommonName != "" {
|
||||
query = append(query, "client_id ILIKE '%' || :client_id || '%'")
|
||||
}
|
||||
|
||||
var emq string
|
||||
if len(query) > 0 {
|
||||
emq = fmt.Sprintf("WHERE %s", strings.Join(query, " AND "))
|
||||
}
|
||||
return emq, nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) retrieveCertificates(ctx context.Context, clientID string, pm certs.PageMetadata) (certs.CertPage, error) {
|
||||
pageQuery, err := PageQuery(pm)
|
||||
if err != nil {
|
||||
return certs.CertPage{}, err
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT client_id, serial_number, expiry_time, revoked FROM certs %s`,
|
||||
pageQuery)
|
||||
|
||||
q = applyLimitOffset(q)
|
||||
|
||||
param := PageMetadata{
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
ClientID: clientID,
|
||||
}
|
||||
|
||||
rows, err := cr.db.NamedQueryContext(ctx, q, param)
|
||||
if err != nil {
|
||||
return certs.CertPage{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
certificates := []certs.Cert{}
|
||||
for rows.Next() {
|
||||
c := certs.Cert{}
|
||||
if err := rows.Scan(&c.ClientID, &c.SerialNumber, &c.ExpiryTime, &c.Revoked); err != nil {
|
||||
return certs.CertPage{}, err
|
||||
}
|
||||
certificates = append(certificates, c)
|
||||
}
|
||||
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) AS total_count
|
||||
FROM certs %s`, pageQuery)
|
||||
|
||||
total, err := postgres.Total(ctx, cr.db, cq, param)
|
||||
if err != nil {
|
||||
return certs.CertPage{}, errors.Wrap(repoerr.ErrFailedOpDB, err)
|
||||
}
|
||||
|
||||
return certs.CertPage{
|
||||
Total: total,
|
||||
Limit: pm.Limit,
|
||||
Offset: pm.Offset,
|
||||
Certificates: certificates,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) RetrieveBySerial(ctx context.Context, serial string) (certs.Cert, error) {
|
||||
q := `SELECT client_id, serial_number, expiry_time, revoked FROM certs WHERE serial_number = $1`
|
||||
var dbcrt dbCert
|
||||
var c certs.Cert
|
||||
|
||||
if err := cr.db.QueryRowxContext(ctx, q, serial).StructScan(&dbcrt); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return c, errors.Wrap(repoerr.ErrNotFound, err)
|
||||
}
|
||||
|
||||
return c, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
c = toCert(dbcrt)
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
type dbCert struct {
|
||||
ClientID string `db:"client_id"`
|
||||
SerialNumber string `db:"serial_number"`
|
||||
ExpiryTime time.Time `db:"expiry_time"`
|
||||
Revoked bool `db:"revoked"`
|
||||
}
|
||||
|
||||
func toDBCert(c certs.Cert) dbCert {
|
||||
return dbCert{
|
||||
ClientID: c.ClientID,
|
||||
SerialNumber: c.SerialNumber,
|
||||
ExpiryTime: c.ExpiryTime,
|
||||
Revoked: c.Revoked,
|
||||
}
|
||||
}
|
||||
|
||||
func toCert(cdb dbCert) certs.Cert {
|
||||
var c certs.Cert
|
||||
c.ClientID = cdb.ClientID
|
||||
c.SerialNumber = cdb.SerialNumber
|
||||
c.ExpiryTime = cdb.ExpiryTime
|
||||
c.Revoked = cdb.Revoked
|
||||
return c
|
||||
}
|
||||
|
||||
func applyLimitOffset(query string) string {
|
||||
return fmt.Sprintf(`%s
|
||||
LIMIT :limit OFFSET :offset`, query)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package postgres contains repository implementations using PostgreSQL as
|
||||
// the underlying database.
|
||||
package postgres
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import migrate "github.com/rubenv/sql-migrate"
|
||||
|
||||
// Migration of Certs service.
|
||||
func Migration() *migrate.MemoryMigrationSource {
|
||||
return &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
{
|
||||
Id: "certs_1",
|
||||
Up: []string{
|
||||
`CREATE TABLE IF NOT EXISTS certs (
|
||||
client_id TEXT NOT NULL,
|
||||
expiry_time TIMESTAMPTZ NOT NULL,
|
||||
serial_number TEXT NOT NULL,
|
||||
revoked BOOLEAN DEFAULT FALSE,
|
||||
PRIMARY KEY (client_id, serial_number)
|
||||
);`,
|
||||
},
|
||||
Down: []string{
|
||||
"DROP TABLE IF EXISTS certs;",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/supermq/certs/postgres"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
pgclient "github.com/absmach/supermq/pkg/postgres"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
)
|
||||
|
||||
var (
|
||||
testLog, _ = smqlog.New(os.Stdout, "info")
|
||||
db *sqlx.DB
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not connect to docker: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
container, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "postgres",
|
||||
Tag: "16.1-alpine",
|
||||
Env: []string{
|
||||
"POSTGRES_USER=test",
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
"listen_addresses = '*'",
|
||||
},
|
||||
}, func(config *docker.HostConfig) {
|
||||
config.AutoRemove = true
|
||||
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
port := container.GetPort("5432/tcp")
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
|
||||
db, err := sql.Open("pgx", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Ping()
|
||||
}); err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not connect to docker: %s", err))
|
||||
}
|
||||
|
||||
dbConfig := pgclient.Config{
|
||||
Host: "localhost",
|
||||
Port: port,
|
||||
User: "test",
|
||||
Pass: "test",
|
||||
Name: "test",
|
||||
SSLMode: "disable",
|
||||
SSLCert: "",
|
||||
SSLKey: "",
|
||||
SSLRootCert: "",
|
||||
}
|
||||
|
||||
if db, err = pgclient.Setup(dbConfig, *postgres.Migration()); err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not setup test DB connection: %s", err))
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// Defers will not be run when using os.Exit
|
||||
db.Close()
|
||||
if err := pool.Purge(container); err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not purge container: %s", err))
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
+79
-64
@@ -7,8 +7,6 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/certs/sdk"
|
||||
pki "github.com/absmach/supermq/certs/pki/amcerts"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
mgsdk "github.com/absmach/supermq/pkg/sdk"
|
||||
@@ -24,6 +22,8 @@ var (
|
||||
ErrFailedToRemoveCertFromDB = errors.New("failed to remove cert serial from db")
|
||||
|
||||
ErrFailedReadFromPKI = errors.New("failed to read certificate from PKI")
|
||||
|
||||
ErrFailedReadFromDB = errors.New("failed to read certificate from database")
|
||||
)
|
||||
|
||||
var _ Service = (*certsService)(nil)
|
||||
@@ -45,24 +45,28 @@ type Service interface {
|
||||
|
||||
// RevokeCert revokes a certificate for a given client ID
|
||||
RevokeCert(ctx context.Context, domainID, token, clientID string) (Revoke, error)
|
||||
}
|
||||
|
||||
type certsService struct {
|
||||
sdk mgsdk.SDK
|
||||
pki pki.Agent
|
||||
}
|
||||
|
||||
// New returns new Certs service.
|
||||
func New(sdk mgsdk.SDK, pkiAgent pki.Agent) Service {
|
||||
return &certsService{
|
||||
sdk: sdk,
|
||||
pki: pkiAgent,
|
||||
}
|
||||
// RevokeBySerial revokes a certificate by its serial number from both PKI and database
|
||||
RevokeBySerial(ctx context.Context, serialID string) (Revoke, error)
|
||||
}
|
||||
|
||||
// Revoke defines the conditions to revoke a certificate.
|
||||
type Revoke struct {
|
||||
RevocationTime time.Time `mapstructure:"revocation_time"`
|
||||
RevocationTime time.Time `json:"revocation_time"`
|
||||
}
|
||||
type certsService struct {
|
||||
sdk mgsdk.SDK
|
||||
certsRepo Repository
|
||||
pki Agent
|
||||
}
|
||||
|
||||
// New returns new Certs service.
|
||||
func New(sdk mgsdk.SDK, certsRepo Repository, pkiAgent Agent) Service {
|
||||
return &certsService{
|
||||
sdk: sdk,
|
||||
pki: pkiAgent,
|
||||
certsRepo: certsRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (cs *certsService) IssueCert(ctx context.Context, domainID, token, clientID, ttl string) (Cert, error) {
|
||||
@@ -78,13 +82,20 @@ func (cs *certsService) IssueCert(ctx context.Context, domainID, token, clientID
|
||||
return Cert{}, errors.Wrap(ErrFailedCertCreation, err)
|
||||
}
|
||||
|
||||
_, err = cs.certsRepo.Save(ctx, cert)
|
||||
if err != nil {
|
||||
return Cert{}, errors.Wrap(ErrFailedCertCreation, err)
|
||||
}
|
||||
|
||||
return Cert{
|
||||
SerialNumber: cert.SerialNumber,
|
||||
Certificate: cert.Certificate,
|
||||
Key: cert.Key,
|
||||
Revoked: cert.Revoked,
|
||||
ExpiryTime: cert.ExpiryTime,
|
||||
IssuingCA: cert.IssuingCA,
|
||||
CAChain: cert.CAChain,
|
||||
ClientID: cert.ClientID,
|
||||
Revoked: cert.Revoked,
|
||||
}, err
|
||||
}
|
||||
|
||||
@@ -92,12 +103,7 @@ func (cs *certsService) RevokeCert(ctx context.Context, domainID, token, clientI
|
||||
var revoke Revoke
|
||||
var err error
|
||||
|
||||
client, err := cs.sdk.Client(ctx, clientID, domainID, token)
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(ErrFailedCertRevocation, err)
|
||||
}
|
||||
|
||||
cp, err := cs.pki.ListCerts(sdk.PageMetadata{Offset: 0, Limit: 10000, EntityID: client.ID})
|
||||
cp, err := cs.certsRepo.RetrieveByClient(ctx, clientID, PageMetadata{Offset: 0, Limit: 10000})
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(ErrFailedCertRevocation, err)
|
||||
}
|
||||
@@ -107,77 +113,86 @@ func (cs *certsService) RevokeCert(ctx context.Context, domainID, token, clientI
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(ErrFailedCertRevocation, err)
|
||||
}
|
||||
|
||||
c.Revoked = true
|
||||
err = cs.certsRepo.Update(ctx, c)
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(ErrFailedReadFromDB, err)
|
||||
}
|
||||
|
||||
revoke.RevocationTime = time.Now().UTC()
|
||||
}
|
||||
|
||||
return revoke, nil
|
||||
}
|
||||
|
||||
func (cs *certsService) RevokeBySerial(ctx context.Context, serialID string) (Revoke, error) {
|
||||
var revoke Revoke
|
||||
|
||||
cert, err := cs.certsRepo.RetrieveBySerial(ctx, serialID)
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(ErrFailedReadFromDB, err)
|
||||
}
|
||||
|
||||
err = cs.pki.Revoke(serialID)
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(ErrFailedCertRevocation, err)
|
||||
}
|
||||
|
||||
cert.Revoked = true
|
||||
err = cs.certsRepo.Update(ctx, cert)
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(ErrFailedReadFromDB, err)
|
||||
}
|
||||
|
||||
revoke.RevocationTime = time.Now().UTC()
|
||||
return revoke, nil
|
||||
}
|
||||
|
||||
func (cs *certsService) ListCerts(ctx context.Context, clientID string, pm PageMetadata) (CertPage, error) {
|
||||
cp, err := cs.pki.ListCerts(sdk.PageMetadata{Offset: pm.Offset, Limit: pm.Limit, EntityID: clientID})
|
||||
cp, err := cs.certsRepo.RetrieveByClient(ctx, clientID, pm)
|
||||
if err != nil {
|
||||
return CertPage{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
var crts []Cert
|
||||
|
||||
for _, c := range cp.Certificates {
|
||||
crts = append(crts, Cert{
|
||||
SerialNumber: c.SerialNumber,
|
||||
Certificate: c.Certificate,
|
||||
Key: c.Key,
|
||||
Revoked: c.Revoked,
|
||||
ExpiryTime: c.ExpiryTime,
|
||||
ClientID: c.ClientID,
|
||||
})
|
||||
for i, cert := range cp.Certificates {
|
||||
vcert, err := cs.pki.View(cert.SerialNumber)
|
||||
if err != nil {
|
||||
return CertPage{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
||||
}
|
||||
cp.Certificates[i].Certificate = vcert.Certificate
|
||||
cp.Certificates[i].Key = vcert.Key
|
||||
}
|
||||
|
||||
return CertPage{
|
||||
Total: cp.Total,
|
||||
Limit: cp.Limit,
|
||||
Offset: cp.Offset,
|
||||
Certificates: crts,
|
||||
}, nil
|
||||
return cp, nil
|
||||
}
|
||||
|
||||
func (cs *certsService) ListSerials(ctx context.Context, clientID string, pm PageMetadata) (CertPage, error) {
|
||||
cp, err := cs.pki.ListCerts(sdk.PageMetadata{Offset: pm.Offset, Limit: pm.Limit, EntityID: clientID})
|
||||
cp, err := cs.certsRepo.RetrieveByClient(ctx, clientID, pm)
|
||||
if err != nil {
|
||||
return CertPage{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
var certs []Cert
|
||||
for _, c := range cp.Certificates {
|
||||
if (pm.Revoked == "true" && c.Revoked) || (pm.Revoked == "false" && !c.Revoked) || (pm.Revoked == "all") {
|
||||
certs = append(certs, Cert{
|
||||
SerialNumber: c.SerialNumber,
|
||||
ClientID: c.ClientID,
|
||||
ExpiryTime: c.ExpiryTime,
|
||||
Revoked: c.Revoked,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return CertPage{
|
||||
Offset: cp.Offset,
|
||||
Limit: cp.Limit,
|
||||
Total: uint64(len(certs)),
|
||||
Certificates: certs,
|
||||
}, nil
|
||||
return cp, nil
|
||||
}
|
||||
|
||||
func (cs *certsService) ViewCert(ctx context.Context, serialID string) (Cert, error) {
|
||||
cert, err := cs.pki.View(serialID)
|
||||
cert, err := cs.certsRepo.RetrieveBySerial(ctx, serialID)
|
||||
if err != nil {
|
||||
return Cert{}, errors.Wrap(ErrFailedReadFromDB, err)
|
||||
}
|
||||
|
||||
vcert, err := cs.pki.View(serialID)
|
||||
if err != nil {
|
||||
return Cert{}, errors.Wrap(ErrFailedReadFromPKI, err)
|
||||
}
|
||||
|
||||
return Cert{
|
||||
SerialNumber: cert.SerialNumber,
|
||||
Certificate: cert.Certificate,
|
||||
Key: cert.Key,
|
||||
Revoked: cert.Revoked,
|
||||
ExpiryTime: cert.ExpiryTime,
|
||||
Certificate: vcert.Certificate,
|
||||
Key: vcert.Key,
|
||||
ExpiryTime: vcert.ExpiryTime,
|
||||
ClientID: cert.ClientID,
|
||||
Revoked: cert.Revoked,
|
||||
}, nil
|
||||
}
|
||||
|
||||
+137
-86
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/absmach/supermq/certs"
|
||||
"github.com/absmach/supermq/certs/mocks"
|
||||
mgcrt "github.com/absmach/supermq/certs/pki/amcerts"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
mgsdk "github.com/absmach/supermq/pkg/sdk"
|
||||
@@ -33,22 +32,22 @@ const (
|
||||
validID = "d4ebb847-5d0e-4e46-bdd9-b6aceaaa3a22"
|
||||
)
|
||||
|
||||
func newService(_ *testing.T) (certs.Service, *mocks.Agent, *sdkmocks.SDK) {
|
||||
func newService(_ *testing.T) (certs.Service, *mocks.Agent, *sdkmocks.SDK, *mocks.Repository) {
|
||||
agent := new(mocks.Agent)
|
||||
repo := new(mocks.Repository)
|
||||
sdk := new(sdkmocks.SDK)
|
||||
|
||||
return certs.New(sdk, agent), agent, sdk
|
||||
return certs.New(sdk, repo, agent), agent, sdk, repo
|
||||
}
|
||||
|
||||
var cert = mgcrt.Cert{
|
||||
var cert = certs.Cert{
|
||||
ClientID: clientID,
|
||||
SerialNumber: "Serial",
|
||||
ExpiryTime: time.Now().Add(time.Duration(1000)),
|
||||
Revoked: false,
|
||||
}
|
||||
|
||||
func TestIssueCert(t *testing.T) {
|
||||
svc, agent, sdk := newService(t)
|
||||
svc, agent, sdk, repo := newService(t)
|
||||
cases := []struct {
|
||||
domainID string
|
||||
token string
|
||||
@@ -57,9 +56,10 @@ func TestIssueCert(t *testing.T) {
|
||||
ttl string
|
||||
ipAddr []string
|
||||
key string
|
||||
cert mgcrt.Cert
|
||||
cert certs.Cert
|
||||
clientErr errors.SDKError
|
||||
issueCertErr error
|
||||
saveErr error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
@@ -108,84 +108,137 @@ func TestIssueCert(t *testing.T) {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
sdkCall := sdk.On("Client", mock.Anything, tc.clientID, tc.domainID, tc.token).Return(mgsdk.Client{ID: tc.clientID, Credentials: mgsdk.ClientCredentials{Secret: clientKey}}, tc.clientErr)
|
||||
agentCall := agent.On("Issue", clientID, tc.ttl, tc.ipAddr).Return(tc.cert, tc.issueCertErr)
|
||||
repoCall := repo.On("Save", mock.Anything, tc.cert).Return("", tc.saveErr)
|
||||
resp, err := svc.IssueCert(context.Background(), tc.domainID, tc.token, tc.clientID, tc.ttl)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.cert.SerialNumber, resp.SerialNumber, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.cert.SerialNumber, resp.SerialNumber))
|
||||
sdkCall.Unset()
|
||||
agentCall.Unset()
|
||||
repoCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevokeCert(t *testing.T) {
|
||||
svc, agent, sdk := newService(t)
|
||||
svc, agent, _, repo := newService(t)
|
||||
cases := []struct {
|
||||
domainID string
|
||||
token string
|
||||
desc string
|
||||
clientID string
|
||||
page mgcrt.CertPage
|
||||
authErr error
|
||||
clientErr errors.SDKError
|
||||
revokeErr error
|
||||
listErr error
|
||||
err error
|
||||
domainID string
|
||||
token string
|
||||
desc string
|
||||
clientID string
|
||||
page certs.CertPage
|
||||
authErr error
|
||||
clientErr errors.SDKError
|
||||
revokeErr error
|
||||
listErr error
|
||||
retrieveErr error
|
||||
updateErr error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "revoke cert",
|
||||
domainID: domain,
|
||||
token: token,
|
||||
clientID: clientID,
|
||||
page: mgcrt.CertPage{Limit: 10000, Offset: 0, Total: 1, Certificates: []mgcrt.Cert{cert}},
|
||||
page: certs.CertPage{Limit: 10000, Offset: 0, Total: 1, Certificates: []certs.Cert{cert}},
|
||||
},
|
||||
{
|
||||
desc: "revoke cert for failed pki revoke",
|
||||
domainID: domain,
|
||||
token: token,
|
||||
clientID: clientID,
|
||||
page: mgcrt.CertPage{Limit: 10000, Offset: 0, Total: 1, Certificates: []mgcrt.Cert{cert}},
|
||||
page: certs.CertPage{Limit: 10000, Offset: 0, Total: 1, Certificates: []certs.Cert{cert}},
|
||||
revokeErr: certs.ErrFailedCertRevocation,
|
||||
err: certs.ErrFailedCertRevocation,
|
||||
},
|
||||
{
|
||||
desc: "revoke cert for invalid client id",
|
||||
domainID: domain,
|
||||
token: token,
|
||||
clientID: "2",
|
||||
page: mgcrt.CertPage{},
|
||||
clientErr: errors.NewSDKError(certs.ErrFailedCertCreation),
|
||||
err: certs.ErrFailedCertRevocation,
|
||||
},
|
||||
{
|
||||
desc: "revoke cert with failed to list certs",
|
||||
domainID: domain,
|
||||
token: token,
|
||||
clientID: clientID,
|
||||
page: mgcrt.CertPage{},
|
||||
listErr: certs.ErrFailedCertRevocation,
|
||||
err: certs.ErrFailedCertRevocation,
|
||||
desc: "revoke cert with failed to list certs",
|
||||
domainID: domain,
|
||||
token: token,
|
||||
clientID: clientID,
|
||||
page: certs.CertPage{},
|
||||
retrieveErr: certs.ErrFailedCertRevocation,
|
||||
listErr: certs.ErrFailedCertRevocation,
|
||||
err: certs.ErrFailedCertRevocation,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
sdkCall := sdk.On("Client", mock.Anything, tc.clientID, tc.domainID, tc.token).Return(mgsdk.Client{ID: tc.clientID, Credentials: mgsdk.ClientCredentials{Secret: clientKey}}, tc.clientErr)
|
||||
repoCall := repo.On("RetrieveByClient", mock.Anything, tc.clientID, mock.Anything).Return(tc.page, tc.retrieveErr)
|
||||
repoCall1 := repo.On("Update", mock.Anything, mock.Anything).Return(tc.updateErr)
|
||||
agentCall := agent.On("Revoke", mock.Anything).Return(tc.revokeErr)
|
||||
agentCall1 := agent.On("ListCerts", mock.Anything).Return(tc.page, tc.listErr)
|
||||
_, err := svc.RevokeCert(context.Background(), tc.domainID, tc.token, tc.clientID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
sdkCall.Unset()
|
||||
repoCall.Unset()
|
||||
repoCall1.Unset()
|
||||
agentCall.Unset()
|
||||
agentCall1.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevokeBySerial(t *testing.T) {
|
||||
svc, agent, _, repo := newService(t)
|
||||
cases := []struct {
|
||||
desc string
|
||||
serialID string
|
||||
revokeErr error
|
||||
updateErr error
|
||||
retrieveErr error
|
||||
Cert certs.Cert
|
||||
expectedTime time.Time
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "revoke cert by serial successfully",
|
||||
serialID: cert.SerialNumber,
|
||||
expectedTime: time.Now(),
|
||||
Cert: certs.Cert{SerialNumber: cert.SerialNumber, ClientID: cert.ClientID, ExpiryTime: cert.ExpiryTime, Revoked: false},
|
||||
},
|
||||
{
|
||||
desc: "revoke cert by serial with PKI revoke failure",
|
||||
serialID: cert.SerialNumber,
|
||||
revokeErr: certs.ErrFailedCertRevocation,
|
||||
err: certs.ErrFailedCertRevocation,
|
||||
},
|
||||
{
|
||||
desc: "revoke cert by serial with repository remove failure",
|
||||
serialID: cert.SerialNumber,
|
||||
updateErr: certs.ErrFailedReadFromDB,
|
||||
err: certs.ErrFailedReadFromDB,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
agentCall := agent.On("Revoke", tc.serialID).Return(tc.revokeErr)
|
||||
repoCall := repo.On("Update", mock.Anything, mock.Anything).Return(tc.updateErr)
|
||||
repoCall1 := repo.On("RetrieveBySerial", mock.Anything, mock.Anything).Return(tc.Cert, tc.retrieveErr)
|
||||
|
||||
result, err := svc.RevokeBySerial(context.Background(), tc.serialID)
|
||||
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
} else {
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.False(t, result.RevocationTime.IsZero(), fmt.Sprintf("%s: revocation time should be set", tc.desc))
|
||||
assert.True(t, time.Since(result.RevocationTime) < time.Minute, fmt.Sprintf("%s: revocation time should be recent", tc.desc))
|
||||
}
|
||||
|
||||
agentCall.Unset()
|
||||
repoCall.Unset()
|
||||
repoCall1.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListCerts(t *testing.T) {
|
||||
svc, agent, _ := newService(t)
|
||||
var mycerts []mgcrt.Cert
|
||||
svc, agent, _, repo := newService(t)
|
||||
var mycerts []certs.Cert
|
||||
for i := 0; i < certNum; i++ {
|
||||
c := mgcrt.Cert{
|
||||
c := certs.Cert{
|
||||
ClientID: clientID,
|
||||
SerialNumber: fmt.Sprintf("%d", i),
|
||||
ExpiryTime: time.Now().Add(time.Hour),
|
||||
@@ -194,95 +247,92 @@ func TestListCerts(t *testing.T) {
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
clientID string
|
||||
page mgcrt.CertPage
|
||||
listErr error
|
||||
err error
|
||||
desc string
|
||||
clientID string
|
||||
page certs.CertPage
|
||||
listErr error
|
||||
retrieveErr error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "list all certs successfully",
|
||||
clientID: clientID,
|
||||
page: mgcrt.CertPage{Limit: certNum, Offset: 0, Total: certNum, Certificates: mycerts},
|
||||
page: certs.CertPage{Limit: certNum, Offset: 0, Total: certNum, Certificates: mycerts},
|
||||
},
|
||||
{
|
||||
desc: "list all certs with failed pki",
|
||||
clientID: clientID,
|
||||
page: mgcrt.CertPage{},
|
||||
listErr: svcerr.ErrViewEntity,
|
||||
err: svcerr.ErrViewEntity,
|
||||
desc: "list all certs with failed pki",
|
||||
clientID: clientID,
|
||||
page: certs.CertPage{},
|
||||
retrieveErr: svcerr.ErrViewEntity,
|
||||
err: svcerr.ErrViewEntity,
|
||||
},
|
||||
{
|
||||
desc: "list half certs successfully",
|
||||
clientID: clientID,
|
||||
page: mgcrt.CertPage{Limit: certNum, Offset: certNum / 2, Total: certNum / 2, Certificates: mycerts[certNum/2:]},
|
||||
page: certs.CertPage{Limit: certNum, Offset: certNum / 2, Total: certNum / 2, Certificates: mycerts[certNum/2:]},
|
||||
},
|
||||
{
|
||||
desc: "list last cert successfully",
|
||||
clientID: clientID,
|
||||
page: mgcrt.CertPage{Limit: certNum, Offset: certNum - 1, Total: 1, Certificates: []mgcrt.Cert{mycerts[certNum-1]}},
|
||||
page: certs.CertPage{Limit: certNum, Offset: certNum - 1, Total: 1, Certificates: []certs.Cert{mycerts[certNum-1]}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
agentCall := agent.On("ListCerts", mock.Anything).Return(tc.page, tc.listErr)
|
||||
repoCall := repo.On("RetrieveByClient", mock.Anything, tc.clientID, mock.Anything, mock.Anything).Return(tc.page, tc.retrieveErr)
|
||||
agentCall := agent.On("View", mock.Anything).Return(certs.Cert{}, tc.listErr)
|
||||
page, err := svc.ListCerts(context.Background(), tc.clientID, certs.PageMetadata{Offset: tc.page.Offset, Limit: tc.page.Limit})
|
||||
size := uint64(len(page.Certificates))
|
||||
assert.Equal(t, tc.page.Total, size, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.page.Total, size))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
repoCall.Unset()
|
||||
agentCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListSerials(t *testing.T) {
|
||||
svc, agent, _ := newService(t)
|
||||
revoke := "false"
|
||||
svc, _, _, repo := newService(t)
|
||||
|
||||
var issuedCerts []mgcrt.Cert
|
||||
var issuedCerts []certs.Cert
|
||||
for i := 0; i < certNum; i++ {
|
||||
crt := mgcrt.Cert{
|
||||
crt := certs.Cert{
|
||||
ClientID: cert.ClientID,
|
||||
SerialNumber: cert.SerialNumber,
|
||||
ExpiryTime: cert.ExpiryTime,
|
||||
Revoked: false,
|
||||
}
|
||||
issuedCerts = append(issuedCerts, crt)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
clientID string
|
||||
revoke string
|
||||
offset uint64
|
||||
limit uint64
|
||||
certs []mgcrt.Cert
|
||||
listErr error
|
||||
err error
|
||||
desc string
|
||||
clientID string
|
||||
offset uint64
|
||||
limit uint64
|
||||
certs []certs.Cert
|
||||
retrieveErr error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "list all certs successfully",
|
||||
clientID: clientID,
|
||||
revoke: revoke,
|
||||
offset: 0,
|
||||
limit: certNum,
|
||||
certs: issuedCerts,
|
||||
},
|
||||
{
|
||||
desc: "list all certs with failed pki",
|
||||
clientID: clientID,
|
||||
revoke: revoke,
|
||||
offset: 0,
|
||||
limit: certNum,
|
||||
certs: nil,
|
||||
listErr: svcerr.ErrViewEntity,
|
||||
err: svcerr.ErrViewEntity,
|
||||
desc: "list all certs with failed pki",
|
||||
clientID: clientID,
|
||||
offset: 0,
|
||||
limit: certNum,
|
||||
certs: nil,
|
||||
retrieveErr: svcerr.ErrViewEntity,
|
||||
err: svcerr.ErrViewEntity,
|
||||
},
|
||||
{
|
||||
desc: "list half certs successfully",
|
||||
clientID: clientID,
|
||||
revoke: revoke,
|
||||
offset: certNum / 2,
|
||||
limit: certNum,
|
||||
certs: issuedCerts[certNum/2:],
|
||||
@@ -290,31 +340,30 @@ func TestListSerials(t *testing.T) {
|
||||
{
|
||||
desc: "list last cert successfully",
|
||||
clientID: clientID,
|
||||
revoke: revoke,
|
||||
offset: certNum - 1,
|
||||
limit: certNum,
|
||||
certs: []mgcrt.Cert{issuedCerts[certNum-1]},
|
||||
certs: []certs.Cert{issuedCerts[certNum-1]},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
agentCall := agent.On("ListCerts", mock.Anything).Return(mgcrt.CertPage{Certificates: tc.certs}, tc.listErr)
|
||||
page, err := svc.ListSerials(context.Background(), tc.clientID, certs.PageMetadata{Revoked: tc.revoke, Offset: tc.offset, Limit: tc.limit})
|
||||
repoCall := repo.On("RetrieveByClient", mock.Anything, tc.clientID, certs.PageMetadata{Offset: tc.offset, Limit: tc.limit}).Return(certs.CertPage{Certificates: tc.certs}, tc.retrieveErr)
|
||||
page, err := svc.ListSerials(context.Background(), tc.clientID, certs.PageMetadata{Offset: tc.offset, Limit: tc.limit})
|
||||
assert.Equal(t, len(tc.certs), len(page.Certificates), fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.certs, page.Certificates))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
agentCall.Unset()
|
||||
repoCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewCert(t *testing.T) {
|
||||
svc, agent, _ := newService(t)
|
||||
svc, agent, _, repo := newService(t)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
serialID string
|
||||
cert mgcrt.Cert
|
||||
cert certs.Cert
|
||||
repoErr error
|
||||
agentErr error
|
||||
err error
|
||||
@@ -327,7 +376,7 @@ func TestViewCert(t *testing.T) {
|
||||
{
|
||||
desc: "list cert with invalid serial",
|
||||
serialID: invalid,
|
||||
cert: mgcrt.Cert{},
|
||||
cert: certs.Cert{},
|
||||
agentErr: svcerr.ErrNotFound,
|
||||
err: svcerr.ErrNotFound,
|
||||
},
|
||||
@@ -335,10 +384,12 @@ func TestViewCert(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
repoCall := repo.On("RetrieveBySerial", mock.Anything, tc.serialID).Return(tc.cert, tc.repoErr)
|
||||
agentCall := agent.On("View", tc.serialID).Return(tc.cert, tc.agentErr)
|
||||
res, err := svc.ViewCert(context.Background(), tc.serialID)
|
||||
assert.Equal(t, tc.cert.SerialNumber, res.SerialNumber, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.cert.SerialNumber, res.SerialNumber))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
repoCall.Unset()
|
||||
agentCall.Unset()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -78,3 +78,13 @@ func (tm *tracingMiddleware) RevokeCert(ctx context.Context, domainID, token, se
|
||||
|
||||
return tm.svc.RevokeCert(ctx, domainID, token, serialID)
|
||||
}
|
||||
|
||||
// RevokeBySerial traces the "RevokeBySerial" operation of the wrapped certs.Service.
|
||||
func (tm *tracingMiddleware) RevokeBySerial(ctx context.Context, serialID string) (certs.Revoke, error) {
|
||||
ctx, span := tracing.StartSpan(ctx, tm.tracer, "svc_revoke_by_serial", trace.WithAttributes(
|
||||
attribute.String("serial_id", serialID),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.RevokeBySerial(ctx, serialID)
|
||||
}
|
||||
|
||||
+18
-1
@@ -35,9 +35,26 @@ var cmdCerts = []cobra.Command{
|
||||
},
|
||||
},
|
||||
{
|
||||
Use: "revoke <client_id> <domain_id> <user_auth_token>",
|
||||
Use: "revoke-all <client_id> <domain_id> <user_auth_token>",
|
||||
Short: "Revoke certificate",
|
||||
Long: `Revokes a certificate for a given client ID.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
logUsageCmd(*cmd, cmd.Use)
|
||||
return
|
||||
}
|
||||
rtime, err := sdk.RevokeAllCerts(cmd.Context(), args[0], args[1], args[2])
|
||||
if err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
logRevokedTimeCmd(*cmd, rtime)
|
||||
},
|
||||
},
|
||||
{
|
||||
Use: "revoke <cert_serial> <domain_id> <user_auth_token>",
|
||||
Short: "Revoke certificate",
|
||||
Long: `Revokes a certificate for a given cert serial.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
logUsageCmd(*cmd, cmd.Use)
|
||||
|
||||
+70
-1
@@ -130,7 +130,7 @@ func TestGetCertCmd(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevokeCertCmd(t *testing.T) {
|
||||
func TestRevokeAllCertCmd(t *testing.T) {
|
||||
sdkMock := new(sdkmocks.SDK)
|
||||
cli.SetSDK(sdkMock)
|
||||
certCmd := cli.NewCertsCmd()
|
||||
@@ -181,6 +181,75 @@ func TestRevokeCertCmd(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
sdkCall := sdkMock.On("RevokeAllCerts", mock.Anything, tc.args[0], tc.args[1], tc.args[2]).Return(tc.time, tc.sdkErr)
|
||||
out := executeCommand(t, rootCmd, append([]string{revokeAllCmd}, tc.args...)...)
|
||||
|
||||
switch tc.logType {
|
||||
case revokeLog:
|
||||
assert.Equal(t, tc.response, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.response, out))
|
||||
case errLog:
|
||||
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
|
||||
case usageLog:
|
||||
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
|
||||
}
|
||||
sdkCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevokeCertCmd(t *testing.T) {
|
||||
sdkMock := new(sdkmocks.SDK)
|
||||
cli.SetSDK(sdkMock)
|
||||
certCmd := cli.NewCertsCmd()
|
||||
rootCmd := setFlags(certCmd)
|
||||
|
||||
revokeTime := time.Now()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
args []string
|
||||
sdkErr errors.SDKError
|
||||
logType outputLog
|
||||
errLogMessage string
|
||||
time time.Time
|
||||
response string
|
||||
}{
|
||||
{
|
||||
desc: "revoke cert successfully",
|
||||
args: []string{
|
||||
cert.SerialNumber,
|
||||
domainID,
|
||||
token,
|
||||
},
|
||||
logType: revokeLog,
|
||||
response: fmt.Sprintf("\nrevoked: %s\n\n", revokeTime),
|
||||
time: revokeTime,
|
||||
},
|
||||
{
|
||||
desc: "revoke cert with invalid args",
|
||||
args: []string{
|
||||
cert.SerialNumber,
|
||||
domainID,
|
||||
token,
|
||||
extraArg,
|
||||
},
|
||||
logType: usageLog,
|
||||
},
|
||||
{
|
||||
desc: "revoke cert with invalid token",
|
||||
args: []string{
|
||||
cert.SerialNumber,
|
||||
domainID,
|
||||
invalidToken,
|
||||
},
|
||||
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
|
||||
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
|
||||
logType: errLog,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
sdkCall := sdkMock.On("RevokeCert", mock.Anything, tc.args[0], tc.args[1], tc.args[2]).Return(tc.time, tc.sdkErr)
|
||||
|
||||
@@ -39,8 +39,9 @@ const (
|
||||
|
||||
// Certs commands
|
||||
const (
|
||||
revokeCmd = "revoke"
|
||||
issueCmd = "issue"
|
||||
revokeCmd = "revoke"
|
||||
revokeAllCmd = "revoke-all"
|
||||
issueCmd = "issue"
|
||||
)
|
||||
|
||||
// Messages commands
|
||||
|
||||
+41
-12
@@ -16,18 +16,22 @@ import (
|
||||
"github.com/absmach/supermq"
|
||||
"github.com/absmach/supermq/certs"
|
||||
httpapi "github.com/absmach/supermq/certs/api"
|
||||
pki "github.com/absmach/supermq/certs/pki/amcerts"
|
||||
pki "github.com/absmach/supermq/certs/pki/openbao"
|
||||
"github.com/absmach/supermq/certs/postgres"
|
||||
"github.com/absmach/supermq/certs/tracing"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
authsvcAuthn "github.com/absmach/supermq/pkg/authn/authsvc"
|
||||
"github.com/absmach/supermq/pkg/grpcclient"
|
||||
jaegerclient "github.com/absmach/supermq/pkg/jaeger"
|
||||
pg "github.com/absmach/supermq/pkg/postgres"
|
||||
pgclient "github.com/absmach/supermq/pkg/postgres"
|
||||
"github.com/absmach/supermq/pkg/prometheus"
|
||||
mgsdk "github.com/absmach/supermq/pkg/sdk"
|
||||
"github.com/absmach/supermq/pkg/server"
|
||||
httpserver "github.com/absmach/supermq/pkg/server/http"
|
||||
"github.com/absmach/supermq/pkg/uuid"
|
||||
"github.com/caarlos0/env/v11"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
@@ -53,10 +57,13 @@ type config struct {
|
||||
SignCAPath string `env:"SMQ_CERTS_SIGN_CA_PATH" envDefault:"ca.crt"`
|
||||
SignCAKeyPath string `env:"SMQ_CERTS_SIGN_CA_KEY_PATH" envDefault:"ca.key"`
|
||||
|
||||
// Amcerts SDK settings
|
||||
SDKHost string `env:"SMQ_CERTS_SDK_HOST" envDefault:""`
|
||||
SDKCertsURL string `env:"SMQ_CERTS_SDK_CERTS_URL" envDefault:"http://localhost:9010"`
|
||||
TLSVerification bool `env:"SMQ_CERTS_SDK_TLS_VERIFICATION" envDefault:"false"`
|
||||
// OpenBao PKI settings
|
||||
OpenBaoHost string `env:"SMQ_CERTS_OPENBAO_HOST" envDefault:"http://localhost:8200"`
|
||||
OpenBaoAppRole string `env:"SMQ_CERTS_OPENBAO_APP_ROLE" envDefault:""`
|
||||
OpenBaoAppSecret string `env:"SMQ_CERTS_OPENBAO_APP_SECRET" envDefault:""`
|
||||
OpenBaoNamespace string `env:"SMQ_CERTS_OPENBAO_NAMESPACE" envDefault:""`
|
||||
OpenBaoPKIPath string `env:"SMQ_CERTS_OPENBAO_PKI_PATH" envDefault:"pki"`
|
||||
OpenBaoRole string `env:"SMQ_CERTS_OPENBAO_ROLE" envDefault:"supermq"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -84,15 +91,21 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.SDKHost == "" {
|
||||
logger.Error("No host specified for PKI engine")
|
||||
if cfg.OpenBaoHost == "" {
|
||||
logger.Error("No host specified for OpenBao PKI engine")
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
pkiclient, err := pki.NewAgent(cfg.SDKHost, cfg.SDKCertsURL, cfg.TLSVerification)
|
||||
if cfg.OpenBaoAppRole == "" || cfg.OpenBaoAppSecret == "" {
|
||||
logger.Error("OpenBao AppRole credentials not specified")
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
pkiclient, err := pki.NewAgent(cfg.OpenBaoAppRole, cfg.OpenBaoAppSecret, cfg.OpenBaoHost, cfg.OpenBaoNamespace, cfg.OpenBaoPKIPath, cfg.OpenBaoRole, logger)
|
||||
if err != nil {
|
||||
logger.Error("failed to configure client for PKI engine")
|
||||
logger.Error("failed to configure client for OpenBao PKI engine")
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
@@ -103,6 +116,20 @@ func main() {
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
dbConfig := pgclient.Config{Name: defDB}
|
||||
if err := env.ParseWithOptions(&dbConfig, env.Options{Prefix: envPrefixDB}); err != nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
migrations := postgres.Migration()
|
||||
db, err := pgclient.Setup(dbConfig, *migrations)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
authn, authnClient, err := authsvcAuthn.NewAuthentication(ctx, grpcCfg)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
@@ -125,7 +152,7 @@ func main() {
|
||||
}()
|
||||
tracer := tp.Tracer(svcName)
|
||||
|
||||
svc := newService(tracer, logger, cfg, pkiclient)
|
||||
svc := newService(db, dbConfig, tracer, logger, cfg, pkiclient)
|
||||
|
||||
httpServerConfig := server.Config{Port: defSvcHTTPPort}
|
||||
if err := env.ParseWithOptions(&httpServerConfig, env.Options{Prefix: envPrefixHTTP}); err != nil {
|
||||
@@ -156,12 +183,14 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func newService(tracer trace.Tracer, logger *slog.Logger, cfg config, pkiAgent pki.Agent) certs.Service {
|
||||
func newService(db *sqlx.DB, dbConfig pgclient.Config, tracer trace.Tracer, logger *slog.Logger, cfg config, pkiAgent pki.Agent) certs.Service {
|
||||
database := pg.NewDatabase(db, dbConfig, tracer)
|
||||
config := mgsdk.Config{
|
||||
ClientsURL: cfg.ClientsURL,
|
||||
}
|
||||
sdk := mgsdk.NewSDK(config)
|
||||
svc := certs.New(sdk, pkiAgent)
|
||||
repo := postgres.NewRepository(database)
|
||||
svc := certs.New(sdk, repo, pkiAgent)
|
||||
svc = httpapi.LoggingMiddleware(svc, logger)
|
||||
counter, latency := prometheus.MakeMetrics(svcName, "api")
|
||||
svc = httpapi.MetricsMiddleware(svc, counter, latency)
|
||||
|
||||
+26
-52
@@ -424,65 +424,18 @@ SMQ_WS_ADAPTER_CACHE_BUFFER_ITEMS=64
|
||||
SMQ_WS_ADAPTER_INSTANCE_ID=
|
||||
|
||||
## Addons Services
|
||||
### Vault
|
||||
SMQ_VAULT_HOST=vault
|
||||
SMQ_VAULT_PORT=8200
|
||||
SMQ_VAULT_ADDR=http://vault:8200
|
||||
SMQ_VAULT_NAMESPACE=supermq
|
||||
SMQ_VAULT_UNSEAL_KEY_1=
|
||||
SMQ_VAULT_UNSEAL_KEY_2=
|
||||
SMQ_VAULT_UNSEAL_KEY_3=
|
||||
SMQ_VAULT_TOKEN=
|
||||
|
||||
SMQ_VAULT_PKI_PATH=pki
|
||||
SMQ_VAULT_PKI_ROLE_NAME=supermq_int_ca
|
||||
SMQ_VAULT_PKI_FILE_NAME=mg_root
|
||||
SMQ_VAULT_PKI_CA_CN='SuperMQ Root Certificate Authority'
|
||||
SMQ_VAULT_PKI_CA_OU='SuperMQ'
|
||||
SMQ_VAULT_PKI_CA_O='SuperMQ'
|
||||
SMQ_VAULT_PKI_CA_C='FRANCE'
|
||||
SMQ_VAULT_PKI_CA_L='PARIS'
|
||||
SMQ_VAULT_PKI_CA_ST='PARIS'
|
||||
SMQ_VAULT_PKI_CA_ADDR='5 Av. Anatole'
|
||||
SMQ_VAULT_PKI_CA_PO='75007'
|
||||
SMQ_VAULT_PKI_CLUSTER_PATH=http://localhost
|
||||
SMQ_VAULT_PKI_CLUSTER_AIA_PATH=http://localhost
|
||||
|
||||
SMQ_VAULT_PKI_INT_PATH=pki_int
|
||||
SMQ_VAULT_PKI_INT_SERVER_CERTS_ROLE_NAME=supermq_server_certs
|
||||
SMQ_VAULT_PKI_INT_CLIENTS_CERTS_ROLE_NAME=supermq_clients_certs
|
||||
SMQ_VAULT_PKI_INT_FILE_NAME=mg_int
|
||||
SMQ_VAULT_PKI_INT_CA_CN='SuperMQ Intermediate Certificate Authority'
|
||||
SMQ_VAULT_PKI_INT_CA_OU='SuperMQ'
|
||||
SMQ_VAULT_PKI_INT_CA_O='SuperMQ'
|
||||
SMQ_VAULT_PKI_INT_CA_C='FRANCE'
|
||||
SMQ_VAULT_PKI_INT_CA_L='PARIS'
|
||||
SMQ_VAULT_PKI_INT_CA_ST='PARIS'
|
||||
SMQ_VAULT_PKI_INT_CA_ADDR='5 Av. Anatole'
|
||||
SMQ_VAULT_PKI_INT_CA_PO='75007'
|
||||
SMQ_VAULT_PKI_INT_CLUSTER_PATH=http://localhost
|
||||
SMQ_VAULT_PKI_INT_CLUSTER_AIA_PATH=http://localhost
|
||||
|
||||
SMQ_VAULT_CLIENTS_CERTS_ISSUER_ROLEID=supermq
|
||||
SMQ_VAULT_CLIENTS_CERTS_ISSUER_SECRET=supermq
|
||||
|
||||
# Certs
|
||||
SMQ_ADDONS_CERTS_PATH_PREFIX=./
|
||||
SMQ_CERTS_LOG_LEVEL=debug
|
||||
SMQ_CERTS_SIGN_CA_PATH=/etc/ssl/certs/ca.crt
|
||||
SMQ_CERTS_SIGN_CA_KEY_PATH=/etc/ssl/certs/ca.key
|
||||
SMQ_CERTS_VAULT_HOST=${SMQ_VAULT_ADDR}
|
||||
SMQ_CERTS_VAULT_NAMESPACE=${SMQ_VAULT_NAMESPACE}
|
||||
SMQ_CERTS_VAULT_APPROLE_ROLEID=${SMQ_VAULT_CLIENTS_CERTS_ISSUER_ROLEID}
|
||||
SMQ_CERTS_VAULT_APPROLE_SECRET=${SMQ_VAULT_CLIENTS_CERTS_ISSUER_SECRET}
|
||||
SMQ_CERTS_VAULT_CLIENTS_CERTS_PKI_PATH=${SMQ_VAULT_PKI_INT_PATH}
|
||||
SMQ_CERTS_VAULT_CLIENTS_CERTS_PKI_ROLE_NAME=${SMQ_VAULT_PKI_INT_CLIENTS_CERTS_ROLE_NAME}
|
||||
SMQ_CERTS_HTTP_HOST=certs
|
||||
SMQ_CERTS_HTTP_PORT=9019
|
||||
SMQ_CERTS_HTTP_SERVER_CERT=
|
||||
SMQ_CERTS_HTTP_SERVER_KEY=
|
||||
SMQ_CERTS_GRPC_HOST=
|
||||
SMQ_CERTS_GRPC_PORT=
|
||||
SMQ_CERTS_DB_HOST=am-certs-db
|
||||
SMQ_CERTS_DB_HOST=certs-db
|
||||
SMQ_CERTS_DB_PORT=5432
|
||||
SMQ_CERTS_DB_USER=supermq
|
||||
SMQ_CERTS_DB_PASS=supermq
|
||||
@@ -492,9 +445,30 @@ SMQ_CERTS_DB_SSL_CERT=
|
||||
SMQ_CERTS_DB_SSL_KEY=
|
||||
SMQ_CERTS_DB_SSL_ROOT_CERT=
|
||||
SMQ_CERTS_INSTANCE_ID=
|
||||
SMQ_CERTS_SDK_HOST=http://supermq-am-certs
|
||||
SMQ_CERTS_SDK_CERTS_URL=${SMQ_CERTS_SDK_HOST}:9010
|
||||
SMQ_CERTS_SDK_TLS_VERIFICATION=false
|
||||
|
||||
### OpenBao
|
||||
SMQ_OPENBAO_HOST=supermq-openbao
|
||||
SMQ_OPENBAO_PORT=8200
|
||||
SMQ_OPENBAO_ADDR=http://supermq-openbao:8200
|
||||
SMQ_OPENBAO_NAMESPACE=supermq
|
||||
SMQ_OPENBAO_UNSEAL_KEY_1=
|
||||
SMQ_OPENBAO_UNSEAL_KEY_2=
|
||||
SMQ_OPENBAO_UNSEAL_KEY_3=
|
||||
SMQ_OPENBAO_TOKEN=
|
||||
SMQ_OPENBAO_ROOT_TOKEN=openbao-root-token
|
||||
SMQ_OPENBAO_APP_ROLE=supermq
|
||||
SMQ_OPENBAO_APP_SECRET=supermq
|
||||
SMQ_OPENBAO_PKI_PATH=pki
|
||||
SMQ_OPENBAO_PKI_ROLE=supermq
|
||||
SMQ_OPENBAO_PKI_CA_CN='SuperMQ Root Certificate Authority'
|
||||
SMQ_OPENBAO_PKI_CA_OU='SuperMQ'
|
||||
SMQ_OPENBAO_PKI_CA_O='SuperMQ'
|
||||
SMQ_OPENBAO_PKI_CA_C='FRANCE'
|
||||
SMQ_OPENBAO_PKI_CA_L='PARIS'
|
||||
SMQ_OPENBAO_PKI_CA_ST='PARIS'
|
||||
SMQ_OPENBAO_PKI_CA_ADDR='5 Av. Anatole'
|
||||
SMQ_OPENBAO_PKI_CA_PO='75007'
|
||||
SMQ_OPENBAO_PKI_ROLE_NAME=supermq
|
||||
|
||||
### Postgres
|
||||
SMQ_POSTGRES_HOST=supermq-postgres
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
# OpenBao Configuration for SuperMQ
|
||||
|
||||
This directory contains both development and production OpenBao configurations for SuperMQ certificate management.
|
||||
|
||||
## Overview
|
||||
|
||||
Two entrypoint scripts are provided:
|
||||
|
||||
- **`dev-entrypoint.sh`**: Development mode with in-memory storage and simple setup
|
||||
- **`prod-entrypoint.sh`**: Production mode with persistent file storage and proper initialization
|
||||
|
||||
Both scripts use environment variables for flexible configuration, allowing you to customize OpenBao behavior without modifying the scripts directly. All configuration is centralized in the `.env` file using the `SMQ_OPENBAO_*` naming convention.
|
||||
|
||||
## Configuration Management
|
||||
|
||||
### Environment-Based Configuration
|
||||
All OpenBao configuration is managed through environment variables defined in `/docker/.env`. This approach provides:
|
||||
|
||||
- **Consistency**: All OpenBao variables use the `SMQ_OPENBAO_*` naming pattern
|
||||
- **Flexibility**: Easy customization without script modifications
|
||||
- **Security**: Sensitive values (tokens, keys) can be externally managed
|
||||
- **Development/Production Parity**: Same configuration approach for both environments
|
||||
|
||||
### Variable Organization
|
||||
Variables are logically grouped by function:
|
||||
- **Core**: Basic OpenBao server configuration
|
||||
- **Authentication**: AppRole and token configuration
|
||||
- **PKI Engine**: Certificate authority and PKI role settings
|
||||
- **PKI CA**: Certificate authority details (CN, organization, etc.)
|
||||
- **Unsealing**: Production unsealing keys and tokens
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Development Mode (Default)
|
||||
```bash
|
||||
docker compose -f docker/docker-compose.yaml -f docker/addons/certs/docker-compose.yaml up -d openbao
|
||||
```
|
||||
|
||||
### Production Mode
|
||||
To switch to production mode, edit `docker-compose.yaml` and change:
|
||||
```yaml
|
||||
- ./dev-entrypoint.sh:/entrypoint.sh
|
||||
```
|
||||
to:
|
||||
```yaml
|
||||
- ./prod-entrypoint.sh:/entrypoint.sh
|
||||
```
|
||||
|
||||
Then start the service:
|
||||
```bash
|
||||
docker compose -f docker/docker-compose.yaml -f docker/addons/certs/docker-compose.yaml up -d openbao
|
||||
```
|
||||
|
||||
## Development Mode Features
|
||||
|
||||
- **In-memory storage**: No data persistence (resets on restart)
|
||||
- **Development server**: Uses `-dev` flag for simple setup
|
||||
- **Hardcoded tokens**: Uses predictable root token for easy access
|
||||
- **Quick setup**: Minimal configuration for development
|
||||
- **No unseal process**: Automatically unsealed
|
||||
|
||||
### Development Access
|
||||
- **Root Token**: `openbao-root-token` (or `SMQ_OPENBAO_ROOT_TOKEN` env var)
|
||||
- **Web UI**: http://localhost:8200/ui
|
||||
- **API**: http://localhost:8200
|
||||
|
||||
## Production Mode Features
|
||||
|
||||
- **File-based storage**: Persistent storage using file backend
|
||||
- **Proper initialization**: Uses unseal keys and root token
|
||||
- **Security policies**: Restricted access policies for PKI operations
|
||||
- **AppRole authentication**: Service-to-service authentication
|
||||
- **PKI engine**: Certificate authority for SuperMQ services
|
||||
- **Automatic unsealing**: Handles unsealing on container restart
|
||||
|
||||
### Production Security
|
||||
|
||||
#### Initial Setup
|
||||
- On first startup, OpenBao will be automatically initialized with 5 unseal keys and 1 root token
|
||||
- The initialization data is stored in `/opt/openbao/data/init.json`
|
||||
- **You must backup this file securely** - it contains the unseal keys and root token
|
||||
|
||||
#### Access Production Instance
|
||||
To get the root token and unseal keys:
|
||||
```bash
|
||||
docker exec supermq-openbao cat /opt/openbao/data/init.json
|
||||
```
|
||||
|
||||
Or to get just the root token:
|
||||
```bash
|
||||
docker exec supermq-openbao jq -r '.root_token' /opt/openbao/data/init.json
|
||||
```
|
||||
|
||||
#### Manual Operations
|
||||
```bash
|
||||
docker exec supermq-openbao bao status
|
||||
|
||||
docker exec supermq-openbao bao operator unseal <unseal-key>
|
||||
|
||||
docker exec supermq-openbao bao operator seal
|
||||
```
|
||||
|
||||
## Configuration Details
|
||||
|
||||
### Development Mode Configuration
|
||||
- **Storage**: In-memory (no persistence)
|
||||
- **Listener**: TCP on `0.0.0.0:8200` (TLS disabled)
|
||||
- **Authentication**: Simple root token
|
||||
- **PKI**: Basic setup for testing
|
||||
|
||||
### Production Mode Configuration
|
||||
- **Storage**: File backend at `/opt/openbao/data`
|
||||
- **Listener**: TCP on `0.0.0.0:8200` (TLS disabled for internal use)
|
||||
- **UI**: Enabled for administration
|
||||
- **Logging**: Info level
|
||||
- **Initialization**: 5 unseal keys, 3 required
|
||||
- **Authentication**: AppRole for services
|
||||
|
||||
### PKI Engine (Both Modes)
|
||||
- **Path**: `/pki`
|
||||
- **Root CA**: SuperMQ Root CA
|
||||
- **Certificate Role**: `supermq` role for service certificates
|
||||
- **Max TTL**: 720 hours (30 days) for dev, 87600 hours (10 years) for root CA in prod
|
||||
|
||||
### AppRole Authentication
|
||||
- **Role**: `supermq`
|
||||
- **Policies**: `pki-policy` (restricted PKI access)
|
||||
- **Token TTL**: 1 hour (renewable up to 4 hours)
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### OpenBao Core Configuration
|
||||
- `SMQ_OPENBAO_HOST`: OpenBao server hostname (default: `supermq-openbao`)
|
||||
- `SMQ_OPENBAO_PORT`: OpenBao server port (default: `8200`)
|
||||
- `SMQ_OPENBAO_ADDR`: Full OpenBao server URL (default: `http://supermq-openbao:8200`)
|
||||
- `SMQ_OPENBAO_NAMESPACE`: OpenBao namespace
|
||||
- `SMQ_OPENBAO_ROOT_TOKEN`: Custom root token for development mode
|
||||
- `SMQ_OPENBAO_TOKEN`: Custom token for production mode
|
||||
- `SMQ_OPENBAO_UNSEAL_KEY_1`: First unseal key for production mode
|
||||
- `SMQ_OPENBAO_UNSEAL_KEY_2`: Second unseal key for production mode
|
||||
- `SMQ_OPENBAO_UNSEAL_KEY_3`: Third unseal key for production mode
|
||||
|
||||
### OpenBao Authentication Configuration
|
||||
- `SMQ_OPENBAO_APP_ROLE`: AppRole role ID for service authentication
|
||||
- `SMQ_OPENBAO_APP_SECRET`: AppRole secret ID for service authentication
|
||||
|
||||
### OpenBao PKI Configuration
|
||||
- `SMQ_OPENBAO_PKI_PATH`: PKI secrets engine path (default: `pki`)
|
||||
- `SMQ_OPENBAO_PKI_ROLE`: PKI role name for certificate issuance (default: `supermq`)
|
||||
- `SMQ_OPENBAO_PKI_ROLE_NAME`: PKI role name for certificate generation (default: `supermq`)
|
||||
|
||||
### OpenBao PKI Certificate Authority Configuration
|
||||
- `SMQ_OPENBAO_PKI_CA_CN`: Certificate Authority Common Name
|
||||
- `SMQ_OPENBAO_PKI_CA_OU`: Certificate Authority Organizational Unit
|
||||
- `SMQ_OPENBAO_PKI_CA_O`: Certificate Authority Organization
|
||||
- `SMQ_OPENBAO_PKI_CA_C`: Certificate Authority Country
|
||||
- `SMQ_OPENBAO_PKI_CA_L`: Certificate Authority Locality
|
||||
- `SMQ_OPENBAO_PKI_CA_ST`: Certificate Authority State/Province
|
||||
- `SMQ_OPENBAO_PKI_CA_ADDR`: Certificate Authority Street Address
|
||||
- `SMQ_OPENBAO_PKI_CA_PO`: Certificate Authority Postal Code
|
||||
|
||||
### Certs Service OpenBao Integration
|
||||
For the SuperMQ certs service, the following variables are used internally:
|
||||
- `SMQ_CERTS_OPENBAO_HOST`: Maps to `SMQ_OPENBAO_HOST` and `SMQ_OPENBAO_PORT`
|
||||
- `SMQ_CERTS_OPENBAO_APP_ROLE`: Maps to `SMQ_OPENBAO_APP_ROLE`
|
||||
- `SMQ_CERTS_OPENBAO_APP_SECRET`: Maps to `SMQ_OPENBAO_APP_SECRET`
|
||||
- `SMQ_CERTS_OPENBAO_NAMESPACE`: Maps to `SMQ_OPENBAO_NAMESPACE`
|
||||
- `SMQ_CERTS_OPENBAO_PKI_PATH`: Maps to `SMQ_OPENBAO_PKI_PATH`
|
||||
- `SMQ_CERTS_OPENBAO_ROLE`: Maps to `SMQ_OPENBAO_PKI_ROLE`
|
||||
|
||||
## Switching Between Modes
|
||||
|
||||
### To Switch to Production Mode:
|
||||
1. Edit `docker-compose.yaml`
|
||||
2. Change `./dev-entrypoint.sh:/entrypoint.sh` to `./prod-entrypoint.sh:/entrypoint.sh`
|
||||
3. Restart the container
|
||||
|
||||
### To Switch to Development Mode:
|
||||
1. Edit `docker-compose.yaml`
|
||||
2. Change `./prod-entrypoint.sh:/entrypoint.sh` to `./dev-entrypoint.sh:/entrypoint.sh`
|
||||
3. Restart the container
|
||||
@@ -1,20 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
common_name: "AbstractMachines_Selfsigned_ca"
|
||||
organization:
|
||||
- "AbstractMacines"
|
||||
organizational_unit:
|
||||
- "AbstractMachines_ca"
|
||||
country:
|
||||
- "France"
|
||||
province:
|
||||
- "Paris"
|
||||
locality:
|
||||
- "Quai de Valmy"
|
||||
postal_code:
|
||||
- "75010 Paris"
|
||||
dns_names:
|
||||
- "localhost"
|
||||
ip_addresses:
|
||||
- "localhost"
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/bin/sh
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -e
|
||||
|
||||
bao server -dev \
|
||||
-dev-root-token-id="${BAO_DEV_ROOT_TOKEN_ID}" \
|
||||
-dev-listen-address="0.0.0.0:8200" \
|
||||
-log-level=info &
|
||||
|
||||
BAO_PID=$!
|
||||
sleep 5
|
||||
|
||||
export BAO_ADDR=http://localhost:8200
|
||||
export BAO_TOKEN="${BAO_DEV_ROOT_TOKEN_ID}"
|
||||
|
||||
if [ -n "$SMQ_OPENBAO_NAMESPACE" ]; then
|
||||
export BAO_NAMESPACE=$SMQ_OPENBAO_NAMESPACE
|
||||
fi
|
||||
|
||||
bao auth enable approle 2>/dev/null || echo "AppRole already enabled"
|
||||
bao secrets enable -path=$SMQ_OPENBAO_PKI_PATH pki 2>/dev/null || echo "PKI already enabled"
|
||||
|
||||
bao secrets tune -max-lease-ttl=87600h $SMQ_OPENBAO_PKI_PATH >/dev/null 2>&1 || true
|
||||
bao write -field=certificate $SMQ_OPENBAO_PKI_PATH/root/generate/internal \
|
||||
common_name="$SMQ_OPENBAO_PKI_CA_CN" \
|
||||
organization="$SMQ_OPENBAO_PKI_CA_O" \
|
||||
ou="$SMQ_OPENBAO_PKI_CA_OU" \
|
||||
country="$SMQ_OPENBAO_PKI_CA_C" \
|
||||
locality="$SMQ_OPENBAO_PKI_CA_L" \
|
||||
province="$SMQ_OPENBAO_PKI_CA_ST" \
|
||||
street_address="$SMQ_OPENBAO_PKI_CA_ADDR" \
|
||||
postal_code="$SMQ_OPENBAO_PKI_CA_PO" \
|
||||
ttl=87600h >/dev/null 2>&1 || true
|
||||
|
||||
bao write $SMQ_OPENBAO_PKI_PATH/config/urls \
|
||||
issuing_certificates="http://localhost:8200/v1/$SMQ_OPENBAO_PKI_PATH/ca" \
|
||||
crl_distribution_points="http://localhost:8200/v1/$SMQ_OPENBAO_PKI_PATH/crl" >/dev/null 2>&1 || true
|
||||
|
||||
bao write $SMQ_OPENBAO_PKI_PATH/roles/$SMQ_OPENBAO_PKI_ROLE_NAME \
|
||||
allow_any_name=true enforce_hostnames=false allow_ip_sans=true \
|
||||
allow_localhost=true max_ttl=720h ttl=720h >/dev/null 2>&1 || true
|
||||
|
||||
cat > /tmp/policy.hcl << EOF
|
||||
path "$SMQ_OPENBAO_PKI_PATH/issue/$SMQ_OPENBAO_PKI_ROLE_NAME" { capabilities = ["create", "update"] }
|
||||
path "$SMQ_OPENBAO_PKI_PATH/certs" { capabilities = ["list"] }
|
||||
path "$SMQ_OPENBAO_PKI_PATH/cert/*" { capabilities = ["read"] }
|
||||
path "$SMQ_OPENBAO_PKI_PATH/revoke" { capabilities = ["create", "update"] }
|
||||
path "auth/token/renew-self" { capabilities = ["update"] }
|
||||
path "auth/token/lookup-self" { capabilities = ["read"] }
|
||||
EOF
|
||||
|
||||
bao policy write pki-policy /tmp/policy.hcl >/dev/null 2>&1 || true
|
||||
|
||||
bao write auth/approle/role/supermq \
|
||||
token_policies=pki-policy token_ttl=1h token_max_ttl=4h renewable=true \
|
||||
bind_secret_id=true >/dev/null 2>&1 || true
|
||||
|
||||
if [ -n "$SMQ_OPENBAO_APP_ROLE" ]; then
|
||||
bao write auth/approle/role/supermq/role-id role_id="$SMQ_OPENBAO_APP_ROLE" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [ -n "$SMQ_OPENBAO_APP_SECRET" ]; then
|
||||
bao write auth/approle/role/supermq/custom-secret-id secret_id="$SMQ_OPENBAO_APP_SECRET" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
echo "OpenBao configuration completed successfully!"
|
||||
echo "OpenBao is ready for SuperMQ on port 8200"
|
||||
echo "Root Token: ${BAO_DEV_ROOT_TOKEN_ID}"
|
||||
|
||||
wait $BAO_PID
|
||||
@@ -11,16 +11,31 @@ networks:
|
||||
name: supermq-base-net
|
||||
external: true
|
||||
|
||||
# Volumes for OpenBao data and configuration
|
||||
volumes:
|
||||
supermq-openbao-volume:
|
||||
supermq-certs-db-volume:
|
||||
|
||||
|
||||
services:
|
||||
certs-db:
|
||||
image: postgres:16.1-alpine
|
||||
container_name: supermq-certs-db
|
||||
restart: on-failure
|
||||
environment:
|
||||
POSTGRES_USER: ${SMQ_CERTS_DB_USER}
|
||||
POSTGRES_PASSWORD: ${SMQ_CERTS_DB_PASS}
|
||||
POSTGRES_DB: ${SMQ_CERTS_DB_NAME}
|
||||
networks:
|
||||
- supermq-base-net
|
||||
volumes:
|
||||
- supermq-certs-db-volume:/var/lib/postgresql/data
|
||||
|
||||
certs:
|
||||
image: supermq/certs:${SMQ_RELEASE_TAG}
|
||||
container_name: supermq-certs
|
||||
depends_on:
|
||||
- am-certs
|
||||
- openbao
|
||||
- certs-db
|
||||
restart: on-failure
|
||||
networks:
|
||||
- supermq-base-net
|
||||
@@ -30,12 +45,12 @@ services:
|
||||
SMQ_CERTS_LOG_LEVEL: ${SMQ_CERTS_LOG_LEVEL}
|
||||
SMQ_CERTS_SIGN_CA_PATH: ${SMQ_CERTS_SIGN_CA_PATH}
|
||||
SMQ_CERTS_SIGN_CA_KEY_PATH: ${SMQ_CERTS_SIGN_CA_KEY_PATH}
|
||||
SMQ_CERTS_VAULT_HOST: ${SMQ_CERTS_VAULT_HOST}
|
||||
SMQ_CERTS_VAULT_NAMESPACE: ${SMQ_CERTS_VAULT_NAMESPACE}
|
||||
SMQ_CERTS_VAULT_APPROLE_ROLEID: ${SMQ_CERTS_VAULT_APPROLE_ROLEID}
|
||||
SMQ_CERTS_VAULT_APPROLE_SECRET: ${SMQ_CERTS_VAULT_APPROLE_SECRET}
|
||||
SMQ_CERTS_VAULT_CLIENTS_CERTS_PKI_PATH: ${SMQ_CERTS_VAULT_CLIENTS_CERTS_PKI_PATH}
|
||||
SMQ_CERTS_VAULT_CLIENTS_CERTS_PKI_ROLE_NAME: ${SMQ_CERTS_VAULT_CLIENTS_CERTS_PKI_ROLE_NAME}
|
||||
SMQ_CERTS_OPENBAO_HOST: http://${SMQ_OPENBAO_HOST}:${SMQ_OPENBAO_PORT}
|
||||
SMQ_CERTS_OPENBAO_APP_ROLE: ${SMQ_OPENBAO_APP_ROLE}
|
||||
SMQ_CERTS_OPENBAO_APP_SECRET: ${SMQ_OPENBAO_APP_SECRET}
|
||||
SMQ_CERTS_OPENBAO_NAMESPACE: ${SMQ_OPENBAO_NAMESPACE}
|
||||
SMQ_CERTS_OPENBAO_PKI_PATH: ${SMQ_OPENBAO_PKI_PATH}
|
||||
SMQ_CERTS_OPENBAO_ROLE: ${SMQ_OPENBAO_PKI_ROLE}
|
||||
SMQ_CERTS_HTTP_HOST: ${SMQ_CERTS_HTTP_HOST}
|
||||
SMQ_CERTS_HTTP_PORT: ${SMQ_CERTS_HTTP_PORT}
|
||||
SMQ_CERTS_HTTP_SERVER_CERT: ${SMQ_CERTS_HTTP_SERVER_CERT}
|
||||
@@ -49,9 +64,6 @@ services:
|
||||
SMQ_CERTS_DB_SSL_CERT: ${SMQ_CERTS_DB_SSL_CERT}
|
||||
SMQ_CERTS_DB_SSL_KEY: ${SMQ_CERTS_DB_SSL_KEY}
|
||||
SMQ_CERTS_DB_SSL_ROOT_CERT: ${SMQ_CERTS_DB_SSL_ROOT_CERT}
|
||||
SMQ_CERTS_SDK_HOST: ${SMQ_CERTS_SDK_HOST}
|
||||
SMQ_CERTS_SDK_CERTS_URL: ${SMQ_CERTS_SDK_CERTS_URL}
|
||||
SMQ_CERTS_SDK_TLS_VERIFICATION: ${SMQ_CERTS_SDK_TLS_VERIFICATION}
|
||||
SMQ_AUTH_GRPC_URL: ${SMQ_AUTH_GRPC_URL}
|
||||
SMQ_AUTH_GRPC_TIMEOUT: ${SMQ_AUTH_GRPC_TIMEOUT}
|
||||
SMQ_AUTH_GRPC_CLIENT_CERT: ${SMQ_AUTH_GRPC_CLIENT_CERT:+/auth-grpc-client.crt}
|
||||
@@ -81,46 +93,41 @@ services:
|
||||
bind:
|
||||
create_host_path: true
|
||||
|
||||
am-certs-db:
|
||||
image: postgres:16.2-alpine
|
||||
container_name: supermq-am-certs-db
|
||||
openbao:
|
||||
image: openbao/openbao:latest
|
||||
container_name: supermq-openbao
|
||||
restart: on-failure
|
||||
networks:
|
||||
- supermq-base-net
|
||||
command: postgres -c "max_connections=${SMQ_POSTGRES_MAX_CONNECTIONS}"
|
||||
environment:
|
||||
POSTGRES_USER: ${SMQ_CERTS_DB_USER}
|
||||
POSTGRES_PASSWORD: ${SMQ_CERTS_DB_PASS}
|
||||
POSTGRES_DB: ${SMQ_CERTS_DB_NAME}
|
||||
ports:
|
||||
- 5454:5432
|
||||
volumes:
|
||||
- supermq-certs-db-volume:/var/lib/postgresql/data
|
||||
|
||||
am-certs:
|
||||
image: ghcr.io/absmach/certs:${SMQ_RELEASE_TAG}
|
||||
container_name: supermq-am-certs
|
||||
depends_on:
|
||||
- am-certs-db
|
||||
restart: on-failure
|
||||
networks:
|
||||
- supermq-base-net
|
||||
- ${SMQ_OPENBAO_PORT}:${SMQ_OPENBAO_PORT}
|
||||
environment:
|
||||
AM_CERTS_LOG_LEVEL: ${SMQ_CERTS_LOG_LEVEL}
|
||||
AM_CERTS_DB_HOST: ${SMQ_CERTS_DB_HOST}
|
||||
AM_CERTS_DB_PORT: ${SMQ_CERTS_DB_PORT}
|
||||
AM_CERTS_DB_USER: ${SMQ_CERTS_DB_USER}
|
||||
AM_CERTS_DB_PASS: ${SMQ_CERTS_DB_PASS}
|
||||
AM_CERTS_DB: ${SMQ_CERTS_DB_NAME}
|
||||
AM_CERTS_DB_SSL_MODE: ${SMQ_CERTS_DB_SSL_MODE}
|
||||
AM_CERTS_HTTP_HOST: supermq-am-certs
|
||||
AM_CERTS_HTTP_PORT: 9010
|
||||
AM_CERTS_GRPC_HOST: supermq-am-certs
|
||||
AM_CERTS_GRPC_PORT: 7012
|
||||
AM_JAEGER_URL: ${SMQ_JAEGER_URL}
|
||||
AM_JAEGER_TRACE_RATIO: ${SMQ_JAEGER_TRACE_RATIO}
|
||||
- BAO_DEV_ROOT_TOKEN_ID=${SMQ_OPENBAO_ROOT_TOKEN}
|
||||
- BAO_ADDR=http://127.0.0.1:${SMQ_OPENBAO_PORT}
|
||||
- SMQ_OPENBAO_PKI_ROLE=${SMQ_OPENBAO_PKI_ROLE}
|
||||
- SMQ_OPENBAO_APP_ROLE=${SMQ_OPENBAO_APP_ROLE}
|
||||
- SMQ_OPENBAO_APP_SECRET=${SMQ_OPENBAO_APP_SECRET}
|
||||
- SMQ_OPENBAO_PORT=${SMQ_OPENBAO_PORT}
|
||||
- SMQ_OPENBAO_NAMESPACE=${SMQ_OPENBAO_NAMESPACE}
|
||||
- SMQ_OPENBAO_UNSEAL_KEY_1=${SMQ_OPENBAO_UNSEAL_KEY_1}
|
||||
- SMQ_OPENBAO_UNSEAL_KEY_2=${SMQ_OPENBAO_UNSEAL_KEY_2}
|
||||
- SMQ_OPENBAO_UNSEAL_KEY_3=${SMQ_OPENBAO_UNSEAL_KEY_3}
|
||||
- SMQ_OPENBAO_TOKEN=${SMQ_OPENBAO_TOKEN}
|
||||
- SMQ_OPENBAO_PKI_CA_CN=${SMQ_OPENBAO_PKI_CA_CN}
|
||||
- SMQ_OPENBAO_PKI_CA_OU=${SMQ_OPENBAO_PKI_CA_OU}
|
||||
- SMQ_OPENBAO_PKI_CA_O=${SMQ_OPENBAO_PKI_CA_O}
|
||||
- SMQ_OPENBAO_PKI_CA_C=${SMQ_OPENBAO_PKI_CA_C}
|
||||
- SMQ_OPENBAO_PKI_CA_L=${SMQ_OPENBAO_PKI_CA_L}
|
||||
- SMQ_OPENBAO_PKI_CA_ST=${SMQ_OPENBAO_PKI_CA_ST}
|
||||
- SMQ_OPENBAO_PKI_CA_ADDR=${SMQ_OPENBAO_PKI_CA_ADDR}
|
||||
- SMQ_OPENBAO_PKI_CA_PO=${SMQ_OPENBAO_PKI_CA_PO}
|
||||
- SMQ_OPENBAO_PKI_ROLE_NAME=${SMQ_OPENBAO_PKI_ROLE_NAME}
|
||||
cap_add:
|
||||
- IPC_LOCK
|
||||
mem_swappiness: 0
|
||||
volumes:
|
||||
- ./config.yaml:/config/config.yaml
|
||||
ports:
|
||||
- 9010:9010
|
||||
- 7012:7012
|
||||
- supermq-openbao-volume:/opt/openbao/data
|
||||
- supermq-openbao-volume:/opt/openbao/config
|
||||
- ./prod-entrypoint.sh:/entrypoint.sh
|
||||
entrypoint: /bin/sh
|
||||
command: /entrypoint.sh
|
||||
|
||||
Executable
+181
@@ -0,0 +1,181 @@
|
||||
#!/bin/sh
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -e
|
||||
|
||||
apk add --no-cache jq
|
||||
|
||||
mkdir -p /opt/openbao/config /opt/openbao/data /opt/openbao/logs
|
||||
|
||||
cat > /opt/openbao/config/config.hcl << 'EOF'
|
||||
storage "file" {
|
||||
path = "/opt/openbao/data"
|
||||
}
|
||||
listener "tcp" {
|
||||
address = "0.0.0.0:8200"
|
||||
tls_disable = true
|
||||
}
|
||||
ui = true
|
||||
log_level = "Info"
|
||||
disable_mlock = true
|
||||
# API timeout settings
|
||||
default_lease_ttl = "168h"
|
||||
max_lease_ttl = "720h"
|
||||
EOF
|
||||
|
||||
export BAO_ADDR=http://127.0.0.1:8200
|
||||
|
||||
if [ -n "$SMQ_OPENBAO_UNSEAL_KEY_1" ] && [ -n "$SMQ_OPENBAO_UNSEAL_KEY_2" ] && [ -n "$SMQ_OPENBAO_UNSEAL_KEY_3" ] && [ -n "$SMQ_OPENBAO_ROOT_TOKEN" ]; then
|
||||
bao server -config=/opt/openbao/config/config.hcl > /opt/openbao/logs/server.log 2>&1 &
|
||||
BAO_PID=$!
|
||||
sleep 5
|
||||
|
||||
bao operator unseal "$SMQ_OPENBAO_UNSEAL_KEY_1"
|
||||
bao operator unseal "$SMQ_OPENBAO_UNSEAL_KEY_2"
|
||||
bao operator unseal "$SMQ_OPENBAO_UNSEAL_KEY_3"
|
||||
|
||||
export BAO_TOKEN=$SMQ_OPENBAO_ROOT_TOKEN
|
||||
else
|
||||
if [ ! -f /opt/openbao/data/init.json ]; then
|
||||
bao server -config=/opt/openbao/config/config.hcl > /opt/openbao/logs/server.log 2>&1 &
|
||||
BAO_PID=$!
|
||||
sleep 5
|
||||
|
||||
bao operator init -key-shares=5 -key-threshold=3 -format=json > /opt/openbao/data/init.json
|
||||
|
||||
UNSEAL_KEY_1=$(cat /opt/openbao/data/init.json | jq -r '.unseal_keys_b64[0]')
|
||||
UNSEAL_KEY_2=$(cat /opt/openbao/data/init.json | jq -r '.unseal_keys_b64[1]')
|
||||
UNSEAL_KEY_3=$(cat /opt/openbao/data/init.json | jq -r '.unseal_keys_b64[2]')
|
||||
ROOT_TOKEN=$(cat /opt/openbao/data/init.json | jq -r '.root_token')
|
||||
|
||||
bao operator unseal "$UNSEAL_KEY_1"
|
||||
bao operator unseal "$UNSEAL_KEY_2"
|
||||
bao operator unseal "$UNSEAL_KEY_3"
|
||||
|
||||
export BAO_TOKEN=$ROOT_TOKEN
|
||||
else
|
||||
bao server -config=/opt/openbao/config/config.hcl > /opt/openbao/logs/server.log 2>&1 &
|
||||
BAO_PID=$!
|
||||
sleep 5
|
||||
|
||||
if bao status | grep -q "Sealed.*true"; then
|
||||
UNSEAL_KEY_1=$(cat /opt/openbao/data/init.json | jq -r '.unseal_keys_b64[0]')
|
||||
UNSEAL_KEY_2=$(cat /opt/openbao/data/init.json | jq -r '.unseal_keys_b64[1]')
|
||||
UNSEAL_KEY_3=$(cat /opt/openbao/data/init.json | jq -r '.unseal_keys_b64[2]')
|
||||
|
||||
bao operator unseal "$UNSEAL_KEY_1"
|
||||
bao operator unseal "$UNSEAL_KEY_2"
|
||||
bao operator unseal "$UNSEAL_KEY_3"
|
||||
fi
|
||||
|
||||
ROOT_TOKEN=$(cat /opt/openbao/data/init.json | jq -r '.root_token')
|
||||
export BAO_TOKEN=$ROOT_TOKEN
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -f /opt/openbao/data/configured ]; then
|
||||
if bao namespace create "$SMQ_OPENBAO_NAMESPACE" 2>/dev/null; then
|
||||
export BAO_NAMESPACE="$SMQ_OPENBAO_NAMESPACE"
|
||||
echo "$SMQ_OPENBAO_NAMESPACE" > /opt/openbao/data/namespace
|
||||
fi
|
||||
|
||||
bao auth enable approle || echo "AppRole already enabled"
|
||||
bao secrets enable -path=pki pki || echo "PKI already enabled"
|
||||
bao secrets tune -max-lease-ttl=87600h pki
|
||||
|
||||
bao write -field=certificate pki/root/generate/internal \
|
||||
common_name="${SMQ_OPENBAO_PKI_CA_CN}" \
|
||||
organization="${SMQ_OPENBAO_PKI_CA_O}" \
|
||||
ou="${SMQ_OPENBAO_PKI_CA_OU}" \
|
||||
country="${SMQ_OPENBAO_PKI_CA_C}" \
|
||||
locality="${SMQ_OPENBAO_PKI_CA_L}" \
|
||||
province="${SMQ_OPENBAO_PKI_CA_ST}" \
|
||||
street_address="${SMQ_OPENBAO_PKI_CA_ADDR}" \
|
||||
postal_code="${SMQ_OPENBAO_PKI_CA_PO}" \
|
||||
ttl=87600h \
|
||||
key_bits=2048 \
|
||||
exclude_cn_from_sans=true
|
||||
|
||||
bao write pki/config/urls \
|
||||
issuing_certificates='http://127.0.0.1:8200/v1/pki/ca' \
|
||||
crl_distribution_points='http://127.0.0.1:8200/v1/pki/crl'
|
||||
|
||||
bao write pki/roles/"${SMQ_OPENBAO_PKI_ROLE_NAME:-supermq}" \
|
||||
allow_any_name=true \
|
||||
enforce_hostnames=false \
|
||||
allow_ip_sans=true \
|
||||
allow_localhost=true \
|
||||
max_ttl=720h \
|
||||
ttl=720h \
|
||||
key_bits=2048
|
||||
|
||||
cat > /opt/openbao/config/pki-policy.hcl << EOF
|
||||
path "pki/issue/${SMQ_OPENBAO_PKI_ROLE_NAME:-supermq}" {
|
||||
capabilities = ["create", "update"]
|
||||
}
|
||||
path "pki/certs" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
path "pki/cert/*" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "pki/revoke" {
|
||||
capabilities = ["create", "update"]
|
||||
}
|
||||
path "auth/token/renew-self" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
path "auth/token/lookup-self" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
EOF
|
||||
|
||||
bao policy write pki-policy /opt/openbao/config/pki-policy.hcl
|
||||
|
||||
bao write auth/approle/role/"${SMQ_OPENBAO_PKI_ROLE_NAME:-supermq}" \
|
||||
token_policies=pki-policy \
|
||||
token_ttl=1h \
|
||||
token_max_ttl=4h \
|
||||
bind_secret_id=true \
|
||||
secret_id_ttl=24h
|
||||
|
||||
if [ -n "$SMQ_OPENBAO_APP_ROLE" ]; then
|
||||
bao write auth/approle/role/"${SMQ_OPENBAO_PKI_ROLE_NAME:-supermq}"/role-id role_id="$SMQ_OPENBAO_APP_ROLE"
|
||||
fi
|
||||
|
||||
if [ -n "$SMQ_OPENBAO_APP_SECRET" ]; then
|
||||
bao write auth/approle/role/"${SMQ_OPENBAO_PKI_ROLE_NAME:-supermq}"/custom-secret-id secret_id="$SMQ_OPENBAO_APP_SECRET"
|
||||
fi
|
||||
|
||||
SERVICE_TOKEN=$(bao write -field=token auth/token/create \
|
||||
policies=pki-policy \
|
||||
ttl=24h \
|
||||
renewable=true \
|
||||
display_name="supermq-service")
|
||||
|
||||
echo "SERVICE_TOKEN=$SERVICE_TOKEN" > /opt/openbao/data/service_token
|
||||
touch /opt/openbao/data/configured
|
||||
echo "OpenBao configuration completed successfully!"
|
||||
else
|
||||
echo "OpenBao already configured, skipping setup..."
|
||||
if [ -f /opt/openbao/data/namespace ] && [ -n "$SMQ_OPENBAO_NAMESPACE" ]; then
|
||||
SAVED_NAMESPACE=$(cat /opt/openbao/data/namespace)
|
||||
if [ "$SAVED_NAMESPACE" = "$SMQ_OPENBAO_NAMESPACE" ]; then
|
||||
export BAO_NAMESPACE="$SMQ_OPENBAO_NAMESPACE"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "================================"
|
||||
echo "OpenBao Production Setup Complete"
|
||||
echo "================================"
|
||||
echo "OpenBao Address: http://localhost:8200"
|
||||
echo "UI Available at: http://localhost:8200/ui"
|
||||
echo "================================"
|
||||
echo "IMPORTANT: Store the init.json file securely!"
|
||||
echo "It contains unseal keys and root token!"
|
||||
echo "================================"
|
||||
|
||||
echo "OpenBao is ready for SuperMQ on port 8200"
|
||||
wait $BAO_PID
|
||||
@@ -1,290 +0,0 @@
|
||||
# Vault
|
||||
|
||||
This is Vault service deployment to be used with SuperMQ.
|
||||
|
||||
When the Vault service is started, some initialization steps need to be done to set clients up.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Variable | Description | Default |
|
||||
| :-------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------- |
|
||||
| SMQ_VAULT_ADDR | Vault Address | http://vault:8200 |
|
||||
| SMQ_VAULT_UNSEAL_KEY_1 | Vault unseal key | "" |
|
||||
| SMQ_VAULT_UNSEAL_KEY_2 | Vault unseal key | "" |
|
||||
| SMQ_VAULT_UNSEAL_KEY_3 | Vault unseal key | "" |
|
||||
| SMQ_VAULT_TOKEN | Vault cli access token | "" |
|
||||
| SMQ_VAULT_PKI_PATH | Vault secrets engine path for Root CA | pki |
|
||||
| SMQ_VAULT_PKI_ROLE_NAME | Vault Root CA role name to issue intermediate CA | supermq_int_ca |
|
||||
| SMQ_VAULT_PKI_FILE_NAME | Root CA Certificates name used by`vault_set_pki.sh` | mg_root |
|
||||
| SMQ_VAULT_PKI_CA_CN | Common name used for Root CA creation by`vault_set_pki.sh` | SuperMQ Root Certificate Authority |
|
||||
| SMQ_VAULT_PKI_CA_OU | Organization unit used for Root CA creation by`vault_set_pki.sh` | SuperMQ |
|
||||
| SMQ_VAULT_PKI_CA_O | Organization used for Root CA creation by`vault_set_pki.sh` | SuperMQ |
|
||||
| SMQ_VAULT_PKI_CA_C | Country used for Root CA creation by`vault_set_pki.sh` | FRANCE |
|
||||
| SMQ_VAULT_PKI_CA_L | Location used for Root CA creation by`vault_set_pki.sh` | PARIS |
|
||||
| SMQ_VAULT_PKI_CA_ST | State or Provisions used for Root CA creation by`vault_set_pki.sh` | PARIS |
|
||||
| SMQ_VAULT_PKI_CA_ADDR | Address used for Root CA creation by`vault_set_pki.sh` | 5 Av. Anatole |
|
||||
| SMQ_VAULT_PKI_CA_PO | Postal code used for Root CA creation by`vault_set_pki.sh` | 75007 |
|
||||
| SMQ_VAULT_PKI_CLUSTER_PATH | Vault Root CA Cluster Path | http://localhost |
|
||||
| SMQ_VAULT_PKI_CLUSTER_AIA_PATH | Vault Root CA Cluster AIA Path | http://localhost |
|
||||
| SMQ_VAULT_PKI_INT_PATH | Vault secrets engine path for Intermediate CA | pki_int |
|
||||
| SMQ_VAULT_PKI_INT_SERVER_CERTS_ROLE_NAME | Vault Intermediate CA role name to issue server certificate | supermq_server_certs |
|
||||
| SMQ_VAULT_PKI_INT_CLIENTS_CERTS_ROLE_NAME | Vault Intermediate CA role name to issue Clients certificates | supermq_clients_certs |
|
||||
| SMQ_VAULT_PKI_INT_FILE_NAME | Intermediate CA Certificates name used by`vault_set_pki.sh` | mg_root |
|
||||
| SMQ_VAULT_PKI_INT_CA_CN | Common name used for Intermediate CA creation by`vault_set_pki.sh` | SuperMQ Root Certificate Authority |
|
||||
| SMQ_VAULT_PKI_INT_CA_OU | Organization unit used for Root CA creation by`vault_set_pki.sh` | SuperMQ |
|
||||
| SMQ_VAULT_PKI_INT_CA_O | Organization used for Intermediate CA creation by`vault_set_pki.sh` | SuperMQ |
|
||||
| SMQ_VAULT_PKI_INT_CA_C | Country used for Intermediate CA creation by`vault_set_pki.sh` | FRANCE |
|
||||
| SMQ_VAULT_PKI_INT_CA_L | Location used for Intermediate CA creation by`vault_set_pki.sh` | PARIS |
|
||||
| SMQ_VAULT_PKI_INT_CA_ST | State or Provisions used for Intermediate CA creation by`vault_set_pki.sh` | PARIS |
|
||||
| SMQ_VAULT_PKI_INT_CA_ADDR | Address used for Intermediate CA creation by`vault_set_pki.sh` | 5 Av. Anatole |
|
||||
| SMQ_VAULT_PKI_INT_CA_PO | Postal code used for Intermediate CA creation by`vault_set_pki.sh` | 75007 |
|
||||
| SMQ_VAULT_PKI_INT_CLUSTER_PATH | Vault Intermediate CA Cluster Path | http://localhost |
|
||||
| SMQ_VAULT_PKI_INT_CLUSTER_AIA_PATH | Vault Intermediate CA Cluster AIA Path | http://localhost |
|
||||
| SMQ_VAULT_CLIENTS_CERTS_ISSUER_ROLEID | Vault Intermediate CA Clients Certificate issuer AppRole authentication RoleID | supermq |
|
||||
| SMQ_VAULT_CLIENTS_CERTS_ISSUER_SECRET | Vault Intermediate CA Clients Certificate issuer AppRole authentication Secret | supermq |
|
||||
|
||||
## Setup
|
||||
|
||||
The following scripts are provided, which work on the running Vault service from within the `docker/addons/vault/scripts` directory.
|
||||
|
||||
### 1. `vault_init.sh`
|
||||
|
||||
Calls `vault operator init` to perform the initial vault initialization and generates a `docker/addons/vault/scripts/data/secrets` file which contains the Vault unseal keys and root tokens.
|
||||
|
||||
### 2. `vault_copy_env.sh`
|
||||
|
||||
After the initial setup, the Vault-related environment variables (`SMQ_VAULT_TOKEN`, `SMQ_VAULT_UNSEAL_KEY_1`, `SMQ_VAULT_UNSEAL_KEY_2`, `SMQ_VAULT_UNSEAL_KEY_3`) need to be updated in the `.env` file.
|
||||
|
||||
The `vault_copy_env.sh` script automatically retrieves these values from the `docker/addons/vault/scripts/data/secrets` file and updates the corresponding environment variables in your `.env` file.
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
Vault environment variables have been successfully set in ~/supermq/docker/.env
|
||||
```
|
||||
|
||||
### 3. `vault_unseal.sh`
|
||||
|
||||
This can be run after the initialization to unseal Vault, which is necessary for it to be used to store and/or get secrets.
|
||||
|
||||
This can be used if you don't want to restart the service.
|
||||
|
||||
The unseal environment variables need to be set in `.env` for the script to work (`SMQ_VAULT_TOKEN`,`SMQ_VAULT_UNSEAL_KEY_1`, `SMQ_VAULT_UNSEAL_KEY_2`, `SMQ_VAULT_UNSEAL_KEY_3`).
|
||||
|
||||
This script should not be necessary to run after the initial setup, since the Vault service unseals itself when starting the container.
|
||||
|
||||
Example output:
|
||||
|
||||
```bash
|
||||
Key Value
|
||||
--- -----
|
||||
Seal Type shamir
|
||||
Initialized true
|
||||
Sealed true
|
||||
Total Shares 5
|
||||
Threshold 3
|
||||
Unseal Progress 1/3
|
||||
Unseal Nonce 4c248cc8-e9f5-055e-319b-00ee06f998a0
|
||||
Version 1.15.4
|
||||
Build Date 2023-12-04T17:45:28Z
|
||||
Storage Type file
|
||||
HA Enabled false
|
||||
Key Value
|
||||
--- -----
|
||||
Seal Type shamir
|
||||
Initialized true
|
||||
Sealed true
|
||||
Total Shares 5
|
||||
Threshold 3
|
||||
Unseal Progress 2/3
|
||||
Unseal Nonce 4c248cc8-e9f5-055e-319b-00ee06f998a0
|
||||
Version 1.15.4
|
||||
Build Date 2023-12-04T17:45:28Z
|
||||
Storage Type file
|
||||
HA Enabled false
|
||||
Key Value
|
||||
--- -----
|
||||
Seal Type shamir
|
||||
Initialized true
|
||||
Sealed false
|
||||
Total Shares 5
|
||||
Threshold 3
|
||||
Unseal Progress 3/3
|
||||
Unseal Nonce 4c248cc8-e9f5-055e-319b-00ee06f998a0
|
||||
Version 1.15.4
|
||||
Build Date 2023-12-04T17:45:28Z
|
||||
Storage Type file
|
||||
HA Enabled false
|
||||
```
|
||||
|
||||
### 4. vault_set_pki.sh
|
||||
|
||||
The `vault_set_pki.sh` script is responsible for generating the root certificate, intermediate certificate, and HTTPS server certificate. All generated certificates, keys, and CSR files are stored in the `docker/addons/vault/scripts/data` directory.
|
||||
|
||||
The script pulls necessary parameters for certificate generation from environment variables, which are, by default, loaded from `docker/.env`.
|
||||
|
||||
- Environment variables prefixed with `SMQ_VAULT_PKI` in the `docker/.env` file are used for generating the root CA.
|
||||
- Environment variables prefixed with `SMQ_VAULT_PKI_INT` are used for generating the intermediate CA.
|
||||
|
||||
To skip generating the server certificate and key, you can pass the `--skip-server-cert` option to the script:
|
||||
|
||||
```sh
|
||||
./vault_set_pki.sh --skip-server-cert
|
||||
```
|
||||
|
||||
#### Troubleshooting:
|
||||
|
||||
If you encounter the following error:
|
||||
|
||||
```sh
|
||||
jq command could not be found, please install it and try again.
|
||||
```
|
||||
|
||||
Install `jq` using:
|
||||
|
||||
```sh
|
||||
sudo apt-get update && sudo apt-get install -y jq
|
||||
```
|
||||
|
||||
After installing `jq`, rerun the script.
|
||||
|
||||
### 5. `vault_create_approle.sh`
|
||||
|
||||
This script enables AppRole authorization in Vault. The certs service uses these AppRole credentials to issue and revoke certificates from the Vault intermediate CA.
|
||||
|
||||
Example output:
|
||||
|
||||
```sh
|
||||
Success! You are now authenticated. The token information displayed below
|
||||
is already stored in the token helper. You do NOT need to run "vault login"
|
||||
again. Future Vault requests will automatically use this token.
|
||||
|
||||
Key Value
|
||||
--- -----
|
||||
token <token_value>
|
||||
token_accessor i6YVeKh4wQ4e0Aj0ONiyGw1Z
|
||||
token_duration ∞
|
||||
token_renewable false
|
||||
token_policies ["root"]
|
||||
identity_policies []
|
||||
policies ["root"]
|
||||
Creating new policy for AppRole
|
||||
Successfully copied 2.56kB to supermq-vault:/vault/supermq_clients_certs_issue.hcl
|
||||
Success! Uploaded policy: supermq_clients_certs_issue
|
||||
Enabling AppRole
|
||||
Success! Enabled approle auth method at: approle/
|
||||
Deleting old AppRole
|
||||
Success! Data deleted (if it existed) at: auth/approle/role/supermq_clients_certs_issuer
|
||||
Creating new AppRole
|
||||
Success! Data written to: auth/approle/role/supermq_clients_certs_issuer
|
||||
Writing custom role ID
|
||||
Key Value
|
||||
--- -----
|
||||
role_id f23942b3-62b9-7456-784f-220ca3f703b9
|
||||
Success! Data written to: auth/approle/role/supermq_clients_certs_issuer/role-id
|
||||
Writing custom secret
|
||||
Key Value
|
||||
--- -----
|
||||
secret_id 61d5a30f-634c-6027-f5b6-4934e6fc49b2
|
||||
secret_id_accessor 1d744f6e-e0c2-5431-a87a-2b23fde584a7
|
||||
secret_id_num_uses 0
|
||||
secret_id_ttl 0s
|
||||
Testing custom role ID and secret by logging in
|
||||
Key Value
|
||||
--- -----
|
||||
token <token_value>
|
||||
token_accessor 9cuwS4mrLHKhJQMv0pl9Bbg9
|
||||
token_duration 1h
|
||||
token_renewable true
|
||||
token_policies ["default" "supermq_clients_certs_issue"]
|
||||
identity_policies []
|
||||
policies ["default" "supermq_clients_certs_issue"]
|
||||
token_meta_role_name supermq_clients_certs_issuer
|
||||
```
|
||||
|
||||
By default, the `vault_create_approle.sh` script tries to enable the AppRole authentication method. Certs service uses the approle credentials to issue and revoke clients certificate from vault intermedate CA. If AppRole is already enabled, you can skip this step by passing the `--skip-enable-approle` argument:
|
||||
|
||||
```sh
|
||||
./vault_create_approle.sh --skip-enable-approle
|
||||
```
|
||||
|
||||
### 6. `vault_copy_certs.sh`
|
||||
|
||||
This script copies the required certificates and keys from `docker/addons/vault/scripts/data` to the `docker/ssl/certs` folder.
|
||||
|
||||
Example output:
|
||||
|
||||
```bash
|
||||
Copying certificate files
|
||||
'data/localhost.crt' -> '~/Documents/supermq/docker/ssl/certs/supermq-server.crt'
|
||||
'data/localhost.key' -> '~/Documents/supermq/docker/ssl/certs/supermq-server.key'
|
||||
'data/mg_int.key' -> '~/Documents/supermq/docker/ssl/certs/ca.key'
|
||||
'data/mg_int_bundle.crt' -> '~/Documents/supermq/docker/ssl/certs/ca.crt'
|
||||
```
|
||||
|
||||
## Custom `.env` Path Support
|
||||
|
||||
Vault scripts support specifying a custom `.env` file path using the `--env-file` argument. If this argument is not provided, the scripts will use the default `.env` file located at `docker/.env`.
|
||||
|
||||
To use a different `.env` file, include the `--env-file` argument followed by the path to your `.env` file when running the Vault scripts. Below are examples of how to execute each script with a custom `.env` file path:
|
||||
|
||||
```bash
|
||||
./vault_init.sh --env-file /custom/path/.env
|
||||
./vault_copy_env.sh --env-file /custom/path/.env
|
||||
./vault_unseal.sh --env-file /custom/path/.env
|
||||
./vault_set_pki.sh --env-file /custom/path/.env
|
||||
./vault_create_approle.sh --env-file /custom/path/.env
|
||||
./vault_copy_certs.sh --env-file /custom/path/.env
|
||||
```
|
||||
|
||||
## Hashicorp Cloud Platform (HCP) Vault
|
||||
|
||||
To have the same PKI setup can done in Hashicorp Cloud Platform (HCP) Vault follow the below steps:
|
||||
Requirement: [VAULT CLI](https://developer.hashicorp.com/vault/tutorials/getting-started/getting-started-install)
|
||||
|
||||
- Replace the environmental variable `SMQ_VAULT_ADDR` in `docker/.env` with HCP Vault address.
|
||||
- Replace the environmental variable `SMQ_VAULT_TOKEN` in `docker/.env` with HCP Vault Admin token.
|
||||
- Run script `vault_set_pki.sh` and `vault_create_approle.sh`.
|
||||
- Optional step, run script `vault_copy_certs.sh` to copy certificates to supermq default path.
|
||||
|
||||
## Vault CLI
|
||||
|
||||
It can also be useful to run the Vault CLI for inspection and administration work.
|
||||
|
||||
```bash
|
||||
Usage: vault <command> [args]
|
||||
|
||||
Common commands:
|
||||
read Read data and retrieves secrets
|
||||
write Write data, configuration, and secrets
|
||||
delete Delete secrets and configuration
|
||||
list List data or secrets
|
||||
login Authenticate locally
|
||||
agent Start a Vault agent
|
||||
server Start a Vault server
|
||||
status Print seal and HA status
|
||||
unwrap Unwrap a wrapped secret
|
||||
|
||||
Other commands:
|
||||
audit Interact with audit devices
|
||||
auth Interact with auth methods
|
||||
debug Runs the debug command
|
||||
kv Interact with Vault's Key-Value storage
|
||||
lease Interact with leases
|
||||
monitor Stream log messages from a Vault server
|
||||
namespace Interact with namespaces
|
||||
operator Perform operator-specific tasks
|
||||
path-help Retrieve API help for paths
|
||||
plugin Interact with Vault plugins and catalog
|
||||
policy Interact with policies
|
||||
print Prints runtime configurations
|
||||
secrets Interact with secrets engines
|
||||
ssh Initiate an SSH session
|
||||
token Interact with tokens
|
||||
```
|
||||
|
||||
If the Vault is setup through `docker/addons/vault`, then Vault CLI can be run directly using the Vault image in Docker: `docker run -it supermq/vault:latest vault`
|
||||
|
||||
## Vault Web UI
|
||||
|
||||
If the Vault is setup through `docker/addons/vault`, Then Vault Web UI is accessible by default on `http://localhost:8200/ui`.
|
||||
@@ -1,10 +0,0 @@
|
||||
storage "file" {
|
||||
path = "/vault/file"
|
||||
}
|
||||
|
||||
listener "tcp" {
|
||||
address = "0.0.0.0:8200"
|
||||
tls_disable = 1
|
||||
}
|
||||
|
||||
ui = true
|
||||
@@ -1,42 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# This docker-compose file contains optional Vault service for SuperMQ platform.
|
||||
# Since this is optional, this file is dependent of docker-compose file
|
||||
# from <project_root>/docker. In order to run these services, execute command:
|
||||
# docker compose -f docker/docker-compose.yaml -f docker/addons/vault/docker-compose.yaml up
|
||||
# from project root. Vault default port (8200) is exposed, so you can use Vault CLI tool for
|
||||
# vault inspection and administration, as well as access the UI.
|
||||
|
||||
networks:
|
||||
supermq-base-net:
|
||||
name: supermq-base-net
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
supermq-vault-volume:
|
||||
|
||||
|
||||
services:
|
||||
vault:
|
||||
image: hashicorp/vault:1.15.4
|
||||
container_name: supermq-vault
|
||||
ports:
|
||||
- ${SMQ_VAULT_PORT}:8200
|
||||
networks:
|
||||
- supermq-base-net
|
||||
volumes:
|
||||
- supermq-vault-volume:/vault/file
|
||||
- supermq-vault-volume:/vault/logs
|
||||
- ./config.hcl:/vault/config/config.hcl
|
||||
- ./entrypoint.sh:/entrypoint.sh
|
||||
environment:
|
||||
VAULT_ADDR: http://127.0.0.1:${SMQ_VAULT_PORT}
|
||||
SMQ_VAULT_PORT: ${SMQ_VAULT_PORT}
|
||||
SMQ_VAULT_UNSEAL_KEY_1: ${SMQ_VAULT_UNSEAL_KEY_1}
|
||||
SMQ_VAULT_UNSEAL_KEY_2: ${SMQ_VAULT_UNSEAL_KEY_2}
|
||||
SMQ_VAULT_UNSEAL_KEY_3: ${SMQ_VAULT_UNSEAL_KEY_3}
|
||||
entrypoint: /bin/sh
|
||||
command: /entrypoint.sh
|
||||
cap_add:
|
||||
- IPC_LOCK
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/usr/bin/dumb-init /bin/sh
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
VAULT_CONFIG_DIR=/vault/config
|
||||
|
||||
docker-entrypoint.sh server &
|
||||
VAULT_PID=$!
|
||||
|
||||
sleep 2
|
||||
|
||||
echo $SMQ_VAULT_UNSEAL_KEY_1
|
||||
echo $SMQ_VAULT_UNSEAL_KEY_2
|
||||
echo $SMQ_VAULT_UNSEAL_KEY_3
|
||||
|
||||
if [[ ! -z "${SMQ_VAULT_UNSEAL_KEY_1}" ]] &&
|
||||
[[ ! -z "${SMQ_VAULT_UNSEAL_KEY_2}" ]] &&
|
||||
[[ ! -z "${SMQ_VAULT_UNSEAL_KEY_3}" ]]; then
|
||||
echo "Unsealing Vault"
|
||||
vault operator unseal ${SMQ_VAULT_UNSEAL_KEY_1}
|
||||
vault operator unseal ${SMQ_VAULT_UNSEAL_KEY_2}
|
||||
vault operator unseal ${SMQ_VAULT_UNSEAL_KEY_3}
|
||||
fi
|
||||
|
||||
wait $VAULT_PID
|
||||
@@ -1,5 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
data
|
||||
supermq_clients_certs_issue.hcl
|
||||
@@ -1,32 +0,0 @@
|
||||
|
||||
# Allow issue certificate with role with default issuer from Intermediate PKI
|
||||
path "${SMQ_VAULT_PKI_INT_PATH}/issue/${SMQ_VAULT_PKI_INT_CLIENTS_CERTS_ROLE_NAME}" {
|
||||
capabilities = ["create", "update"]
|
||||
}
|
||||
|
||||
## Revole certificate from Intermediate PKI
|
||||
path "${SMQ_VAULT_PKI_INT_PATH}/revoke" {
|
||||
capabilities = ["create", "update"]
|
||||
}
|
||||
|
||||
## List Revoked Certificates from Intermediate PKI
|
||||
path "${SMQ_VAULT_PKI_INT_PATH}/certs/revoked" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
|
||||
|
||||
## List Certificates from Intermediate PKI
|
||||
path "${SMQ_VAULT_PKI_INT_PATH}/certs" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
|
||||
## Read Certificate from Intermediate PKI
|
||||
path "${SMQ_VAULT_PKI_INT_PATH}/cert/+" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "${SMQ_VAULT_PKI_INT_PATH}/cert/+/raw" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "${SMQ_VAULT_PKI_INT_PATH}/cert/+/raw/pem" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
vault() {
|
||||
if is_container_running "supermq-vault"; then
|
||||
docker exec -it supermq-vault vault "$@"
|
||||
else
|
||||
if which vault &> /dev/null; then
|
||||
$(which vault) "$@"
|
||||
else
|
||||
echo "supermq-vault container or vault command not found. Please refer to the documentation: https://github.com/absmach/supermq/blob/main/docker/addons/vault/README.md"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
is_container_running() {
|
||||
local container_name="$1"
|
||||
if [ "$(docker inspect --format '{{.State.Running}}' "$container_name" 2>/dev/null)" = "true" ]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
scriptdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
||||
|
||||
# default env file path
|
||||
env_file="docker/.env"
|
||||
|
||||
# default certs copy path
|
||||
certs_copy_path="docker/ssl/certs/"
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
--env-file)
|
||||
if [[ -z "${2:-}" ]]; then
|
||||
echo "Error: --env-file requires a non-empty option argument."
|
||||
exit 1
|
||||
fi
|
||||
env_file="$2"
|
||||
if [[ ! -f "$env_file" ]]; then
|
||||
echo "Error: .env file not found at $env_file"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
--certs-copy-path)
|
||||
if [[ -z "${2:-}" ]]; then
|
||||
echo "Error: --certs-copy-path requires a non-empty option argument."
|
||||
exit 1
|
||||
fi
|
||||
certs_copy_path="$2"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown parameter passed: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
readDotEnv() {
|
||||
set -o allexport
|
||||
source "$env_file"
|
||||
set +o allexport
|
||||
}
|
||||
|
||||
readDotEnv
|
||||
|
||||
server_name="localhost"
|
||||
|
||||
# Check if SMQ_NGINX_SERVER_NAME is set or not empty
|
||||
if [ -n "${SMQ_NGINX_SERVER_NAME:-}" ]; then
|
||||
server_name="$SMQ_NGINX_SERVER_NAME"
|
||||
fi
|
||||
|
||||
echo "Copying certificate files to ${certs_copy_path}"
|
||||
|
||||
if [ -e "$scriptdir/data/${server_name}.crt" ]; then
|
||||
cp -v "$scriptdir/data/${server_name}.crt" "${certs_copy_path}supermq-server.crt"
|
||||
else
|
||||
echo "${server_name}.crt file not available"
|
||||
fi
|
||||
|
||||
if [ -e "$scriptdir/data/${server_name}.key" ]; then
|
||||
cp -v "$scriptdir/data/${server_name}.key" "${certs_copy_path}supermq-server.key"
|
||||
else
|
||||
echo "${server_name}.key file not available"
|
||||
fi
|
||||
|
||||
if [ -e "$scriptdir/data/${SMQ_VAULT_PKI_INT_FILE_NAME}.key" ]; then
|
||||
cp -v "$scriptdir/data/${SMQ_VAULT_PKI_INT_FILE_NAME}.key" "${certs_copy_path}ca.key"
|
||||
else
|
||||
echo "$scriptdir/data/${SMQ_VAULT_PKI_INT_FILE_NAME}.key file not available"
|
||||
fi
|
||||
|
||||
if [ -e "$scriptdir/data/${SMQ_VAULT_PKI_INT_FILE_NAME}_bundle.crt" ]; then
|
||||
cp -v "$scriptdir/data/${SMQ_VAULT_PKI_INT_FILE_NAME}_bundle.crt" "${certs_copy_path}ca.crt"
|
||||
else
|
||||
echo "$scriptdir/data/${SMQ_VAULT_PKI_INT_FILE_NAME}_bundle.crt file not available"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -1,52 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
scriptdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
||||
|
||||
# default env file path
|
||||
env_file="docker/.env"
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
--env-file)
|
||||
if [[ -z "${2:-}" ]]; then
|
||||
echo "Error: --env-file requires a non-empty option argument."
|
||||
exit 1
|
||||
fi
|
||||
env_file="$2"
|
||||
if [[ ! -f "$env_file" ]]; then
|
||||
echo "Error: .env file not found at $env_file"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown parameter passed: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
write_env() {
|
||||
if [ -e "$scriptdir/data/secrets" ]; then
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
SED_OPT=(-i '')
|
||||
else
|
||||
SED_OPT=(-i)
|
||||
fi
|
||||
|
||||
sed "${SED_OPT[@]}" "s,SMQ_VAULT_UNSEAL_KEY_1=.*,SMQ_VAULT_UNSEAL_KEY_1=$(awk -F ': ' '$1 == "Unseal Key 1" {print $2}' "$scriptdir/data/secrets")," "$env_file"
|
||||
sed "${SED_OPT[@]}" "s,SMQ_VAULT_UNSEAL_KEY_2=.*,SMQ_VAULT_UNSEAL_KEY_2=$(awk -F ': ' '$1 == "Unseal Key 2" {print $2}' "$scriptdir/data/secrets")," "$env_file"
|
||||
sed "${SED_OPT[@]}" "s,SMQ_VAULT_UNSEAL_KEY_3=.*,SMQ_VAULT_UNSEAL_KEY_3=$(awk -F ': ' '$1 == "Unseal Key 3" {print $2}' "$scriptdir/data/secrets")," "$env_file"
|
||||
sed "${SED_OPT[@]}" "s,SMQ_VAULT_TOKEN=.*,SMQ_VAULT_TOKEN=$(awk -F ': ' '$1 == "Initial Root Token" {print $2}' "$scriptdir/data/secrets")," "$env_file"
|
||||
echo "Vault environment variables are set successfully in $env_file"
|
||||
else
|
||||
echo "Error: Source file '$scriptdir/data/secrets' not found."
|
||||
fi
|
||||
}
|
||||
|
||||
write_env
|
||||
@@ -1,122 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
scriptdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
||||
|
||||
# default env file path
|
||||
env_file="docker/.env"
|
||||
|
||||
SKIP_ENABLE_APP_ROLE=""
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
--env-file)
|
||||
if [[ -z "${2:-}" ]]; then
|
||||
echo "Error: --env-file requires a non-empty option argument."
|
||||
exit 1
|
||||
fi
|
||||
env_file="$2"
|
||||
if [[ ! -f "$env_file" ]]; then
|
||||
echo "Error: .env file not found at $env_file"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
--skip-enable-approle)
|
||||
SKIP_ENABLE_APP_ROLE="true"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown parameter passed: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
readDotEnv() {
|
||||
set -o allexport
|
||||
source "$env_file"
|
||||
set +o allexport
|
||||
}
|
||||
|
||||
source "$scriptdir/vault_cmd.sh"
|
||||
|
||||
vaultCreatePolicyFile() {
|
||||
envsubst '
|
||||
${SMQ_VAULT_PKI_INT_PATH}
|
||||
${SMQ_VAULT_PKI_INT_CLIENTS_CERTS_ROLE_NAME}
|
||||
' < "$scriptdir/supermq_clients_certs_issue.template.hcl" > "$scriptdir/supermq_clients_certs_issue.hcl"
|
||||
}
|
||||
|
||||
vaultCreatePolicy() {
|
||||
echo "Creating new policy for AppRole"
|
||||
if is_container_running "supermq-vault"; then
|
||||
docker cp "$scriptdir/supermq_clients_certs_issue.hcl" supermq-vault:/vault/supermq_clients_certs_issue.hcl
|
||||
vault policy write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} supermq_clients_certs_issue /vault/supermq_clients_certs_issue.hcl
|
||||
else
|
||||
vault policy write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} supermq_clients_certs_issue "$scriptdir/supermq_clients_certs_issue.hcl"
|
||||
fi
|
||||
}
|
||||
|
||||
vaultEnableAppRole() {
|
||||
if [[ "$SKIP_ENABLE_APP_ROLE" == "true" ]]; then
|
||||
echo "Skipping Enable AppRole"
|
||||
else
|
||||
echo "Enabling AppRole"
|
||||
vault auth enable -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} approle
|
||||
fi
|
||||
}
|
||||
|
||||
vaultDeleteRole() {
|
||||
echo "Deleting old AppRole"
|
||||
vault delete -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} auth/approle/role/supermq_clients_certs_issuer
|
||||
}
|
||||
|
||||
vaultCreateRole() {
|
||||
echo "Creating new AppRole"
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} auth/approle/role/supermq_clients_certs_issuer \
|
||||
token_policies=supermq_clients_certs_issue secret_id_num_uses=0 \
|
||||
secret_id_ttl=0 token_ttl=1h token_max_ttl=3h token_num_uses=0
|
||||
}
|
||||
|
||||
vaultWriteCustomRoleID() {
|
||||
echo "Writing custom role id"
|
||||
vault read -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} auth/approle/role/supermq_clients_certs_issuer/role-id
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} auth/approle/role/supermq_clients_certs_issuer/role-id role_id=${SMQ_VAULT_CLIENTS_CERTS_ISSUER_ROLEID}
|
||||
}
|
||||
|
||||
vaultWriteCustomSecret() {
|
||||
echo "Writing custom secret"
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} -f auth/approle/role/supermq_clients_certs_issuer/secret-id
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} auth/approle/role/supermq_clients_certs_issuer/custom-secret-id secret_id=${SMQ_VAULT_CLIENTS_CERTS_ISSUER_SECRET} num_uses=0 ttl=0
|
||||
}
|
||||
|
||||
vaultTestRoleLogin() {
|
||||
echo "Testing custom roleid secret by logging in"
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} auth/approle/login \
|
||||
role_id=${SMQ_VAULT_CLIENTS_CERTS_ISSUER_ROLEID} \
|
||||
secret_id=${SMQ_VAULT_CLIENTS_CERTS_ISSUER_SECRET}
|
||||
}
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "jq command could not be found, please install it and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readDotEnv
|
||||
|
||||
vault login -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} ${SMQ_VAULT_TOKEN}
|
||||
|
||||
vaultCreatePolicyFile
|
||||
vaultCreatePolicy
|
||||
vaultEnableAppRole
|
||||
vaultDeleteRole
|
||||
vaultCreateRole
|
||||
vaultWriteCustomRoleID
|
||||
vaultWriteCustomSecret
|
||||
vaultTestRoleLogin
|
||||
|
||||
exit 0
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
scriptdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
||||
|
||||
# default env file path
|
||||
env_file="docker/.env"
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
--env-file)
|
||||
if [[ -z "${2:-}" ]]; then
|
||||
echo "Error: --env-file requires a non-empty option argument."
|
||||
exit 1
|
||||
fi
|
||||
env_file="$2"
|
||||
if [[ ! -f "$env_file" ]]; then
|
||||
echo "Error: .env file not found at $env_file"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown parameter passed: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
readDotEnv() {
|
||||
set -o allexport
|
||||
source "$env_file"
|
||||
set +o allexport
|
||||
}
|
||||
|
||||
source "$scriptdir/vault_cmd.sh"
|
||||
|
||||
readDotEnv
|
||||
|
||||
mkdir -p "$scriptdir/data"
|
||||
|
||||
vault operator init -address="$SMQ_VAULT_ADDR" 2>&1 | tee >(sed -r 's/\x1b\[[0-9;]*m//g' > "$scriptdir/data/secrets")
|
||||
@@ -1,252 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
scriptdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
||||
|
||||
# edfault env file path
|
||||
env_file="docker/.env"
|
||||
|
||||
SKIP_SERVER_CERT=""
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
--env-file)
|
||||
if [[ -z "${2:-}" ]]; then
|
||||
echo "Error: --env-file requires a non-empty option argument."
|
||||
exit 1
|
||||
fi
|
||||
env_file="$2"
|
||||
if [[ ! -f "$env_file" ]]; then
|
||||
echo "Error: .env file not found at $env_file"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
--skip-server-cert)
|
||||
SKIP_SERVER_CERT="--skip-server-cert"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown parameter passed: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
readDotEnv() {
|
||||
set -o allexport
|
||||
source "$env_file"
|
||||
set +o allexport
|
||||
}
|
||||
|
||||
readDotEnv
|
||||
|
||||
server_name="localhost"
|
||||
|
||||
# Check if SMQ_NGINX_SERVER_NAME is set or not empty
|
||||
if [ -n "${SMQ_NGINX_SERVER_NAME:-}" ]; then
|
||||
server_name="$SMQ_NGINX_SERVER_NAME"
|
||||
fi
|
||||
|
||||
source "$scriptdir/vault_cmd.sh"
|
||||
|
||||
vaultEnablePKI() {
|
||||
vault secrets enable -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} -path ${SMQ_VAULT_PKI_PATH} pki
|
||||
vault secrets tune -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} -max-lease-ttl=87600h ${SMQ_VAULT_PKI_PATH}
|
||||
}
|
||||
|
||||
vaultConfigPKIClusterPath() {
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} ${SMQ_VAULT_PKI_PATH}/config/cluster aia_path=${SMQ_VAULT_PKI_CLUSTER_AIA_PATH} path=${SMQ_VAULT_PKI_CLUSTER_PATH}
|
||||
}
|
||||
|
||||
vaultConfigPKICrl() {
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} ${SMQ_VAULT_PKI_PATH}/config/crl expiry="5m" ocsp_disable=false ocsp_expiry=0 auto_rebuild=true auto_rebuild_grace_period="2m" enable_delta=true delta_rebuild_interval="1m"
|
||||
}
|
||||
|
||||
vaultAddRoleToSecret() {
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} ${SMQ_VAULT_PKI_PATH}/roles/${SMQ_VAULT_PKI_ROLE_NAME} \
|
||||
allow_any_name=true \
|
||||
max_ttl="8760h" \
|
||||
default_ttl="8760h" \
|
||||
generate_lease=true
|
||||
}
|
||||
|
||||
vaultGenerateRootCACertificate() {
|
||||
echo "Generate root CA certificate"
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} -format=json ${SMQ_VAULT_PKI_PATH}/root/generate/exported \
|
||||
common_name="\"$SMQ_VAULT_PKI_CA_CN\"" \
|
||||
ou="\"$SMQ_VAULT_PKI_CA_OU\"" \
|
||||
organization="\"$SMQ_VAULT_PKI_CA_O\"" \
|
||||
country="\"$SMQ_VAULT_PKI_CA_C\"" \
|
||||
locality="\"$SMQ_VAULT_PKI_CA_L\"" \
|
||||
province="\"$SMQ_VAULT_PKI_CA_ST\"" \
|
||||
street_address="\"$SMQ_VAULT_PKI_CA_ADDR\"" \
|
||||
postal_code="\"$SMQ_VAULT_PKI_CA_PO\"" \
|
||||
ttl=87600h | tee >(jq -r .data.certificate >"$scriptdir/data/${SMQ_VAULT_PKI_FILE_NAME}_ca.crt") \
|
||||
>(jq -r .data.issuing_ca >"$scriptdir/data/${SMQ_VAULT_PKI_FILE_NAME}_issuing_ca.crt") \
|
||||
>(jq -r .data.private_key >"$scriptdir/data/${SMQ_VAULT_PKI_FILE_NAME}_ca.key")
|
||||
}
|
||||
|
||||
vaultSetupRootCAIssuingURLs() {
|
||||
echo "Setup URLs for CRL and issuing"
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} ${SMQ_VAULT_PKI_PATH}/config/urls \
|
||||
issuing_certificates="{{cluster_aia_path}}/v1/${SMQ_VAULT_PKI_PATH}/ca" \
|
||||
crl_distribution_points="{{cluster_aia_path}}/v1/${SMQ_VAULT_PKI_PATH}/crl" \
|
||||
ocsp_servers="{{cluster_aia_path}}/v1/${SMQ_VAULT_PKI_PATH}/ocsp" \
|
||||
enable_templating=true
|
||||
}
|
||||
|
||||
vaultGenerateIntermediateCAPKI() {
|
||||
echo "Generate Intermediate CA PKI"
|
||||
vault secrets enable -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} -path=${SMQ_VAULT_PKI_INT_PATH} pki
|
||||
vault secrets tune -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} -max-lease-ttl=43800h ${SMQ_VAULT_PKI_INT_PATH}
|
||||
}
|
||||
|
||||
vaultConfigIntermediatePKIClusterPath() {
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} ${SMQ_VAULT_PKI_INT_PATH}/config/cluster aia_path=${SMQ_VAULT_PKI_INT_CLUSTER_AIA_PATH} path=${SMQ_VAULT_PKI_INT_CLUSTER_PATH}
|
||||
}
|
||||
|
||||
vaultConfigIntermediatePKICrl() {
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} ${SMQ_VAULT_PKI_INT_PATH}/config/crl expiry="5m" ocsp_disable=false ocsp_expiry=0 auto_rebuild=true auto_rebuild_grace_period="2m" enable_delta=true delta_rebuild_interval="1m"
|
||||
}
|
||||
|
||||
vaultGenerateIntermediateCSR() {
|
||||
echo "Generate intermediate CSR"
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} -format=json ${SMQ_VAULT_PKI_INT_PATH}/intermediate/generate/exported \
|
||||
common_name="\"$SMQ_VAULT_PKI_INT_CA_CN\"" \
|
||||
ou="\"$SMQ_VAULT_PKI_INT_CA_OU\""\
|
||||
organization="\"$SMQ_VAULT_PKI_INT_CA_O\"" \
|
||||
country="\"$SMQ_VAULT_PKI_INT_CA_C\"" \
|
||||
locality="\"$SMQ_VAULT_PKI_INT_CA_L\"" \
|
||||
province="\"$SMQ_VAULT_PKI_INT_CA_ST\"" \
|
||||
street_address="\"$SMQ_VAULT_PKI_INT_CA_ADDR\"" \
|
||||
postal_code="\"$SMQ_VAULT_PKI_INT_CA_PO\"" \
|
||||
| tee >(jq -r .data.csr >"$scriptdir/data/${SMQ_VAULT_PKI_INT_FILE_NAME}.csr") \
|
||||
>(jq -r .data.private_key >"$scriptdir/data/${SMQ_VAULT_PKI_INT_FILE_NAME}.key")
|
||||
}
|
||||
|
||||
vaultSignIntermediateCSR() {
|
||||
echo "Sign intermediate CSR"
|
||||
if is_container_running "supermq-vault"; then
|
||||
docker cp "$scriptdir/data/${SMQ_VAULT_PKI_INT_FILE_NAME}.csr" supermq-vault:/vault/${SMQ_VAULT_PKI_INT_FILE_NAME}.csr
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} -format=json ${SMQ_VAULT_PKI_PATH}/root/sign-intermediate \
|
||||
csr=@/vault/${SMQ_VAULT_PKI_INT_FILE_NAME}.csr ttl="8760h" \
|
||||
ou="\"$SMQ_VAULT_PKI_INT_CA_OU\""\
|
||||
organization="\"$SMQ_VAULT_PKI_INT_CA_O\"" \
|
||||
country="\"$SMQ_VAULT_PKI_INT_CA_C\"" \
|
||||
locality="\"$SMQ_VAULT_PKI_INT_CA_L\"" \
|
||||
province="\"$SMQ_VAULT_PKI_INT_CA_ST\"" \
|
||||
street_address="\"$SMQ_VAULT_PKI_INT_CA_ADDR\"" \
|
||||
postal_code="\"$SMQ_VAULT_PKI_INT_CA_PO\"" \
|
||||
| tee >(jq -r .data.certificate >"$scriptdir/data/${SMQ_VAULT_PKI_INT_FILE_NAME}.crt") \
|
||||
>(jq -r .data.issuing_ca >"$scriptdir/data/${SMQ_VAULT_PKI_INT_FILE_NAME}_issuing_ca.crt")
|
||||
else
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} -format=json ${SMQ_VAULT_PKI_PATH}/root/sign-intermediate \
|
||||
csr=@"$scriptdir/data/${SMQ_VAULT_PKI_INT_FILE_NAME}.csr" ttl="8760h" \
|
||||
ou="\"$SMQ_VAULT_PKI_INT_CA_OU\""\
|
||||
organization="\"$SMQ_VAULT_PKI_INT_CA_O\"" \
|
||||
country="\"$SMQ_VAULT_PKI_INT_CA_C\"" \
|
||||
locality="\"$SMQ_VAULT_PKI_INT_CA_L\"" \
|
||||
province="\"$SMQ_VAULT_PKI_INT_CA_ST\"" \
|
||||
street_address="\"$SMQ_VAULT_PKI_INT_CA_ADDR\"" \
|
||||
postal_code="\"$SMQ_VAULT_PKI_INT_CA_PO\"" \
|
||||
| tee >(jq -r .data.certificate >"$scriptdir/data/${SMQ_VAULT_PKI_INT_FILE_NAME}.crt") \
|
||||
>(jq -r .data.issuing_ca >"$scriptdir/data/${SMQ_VAULT_PKI_INT_FILE_NAME}_issuing_ca.crt")
|
||||
fi
|
||||
}
|
||||
|
||||
vaultInjectIntermediateCertificate() {
|
||||
echo "Inject Intermediate Certificate"
|
||||
if is_container_running "supermq-vault"; then
|
||||
docker cp "$scriptdir/data/${SMQ_VAULT_PKI_INT_FILE_NAME}.crt" supermq-vault:/vault/${SMQ_VAULT_PKI_INT_FILE_NAME}.crt
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} ${SMQ_VAULT_PKI_INT_PATH}/intermediate/set-signed certificate=@/vault/${SMQ_VAULT_PKI_INT_FILE_NAME}.crt
|
||||
else
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} ${SMQ_VAULT_PKI_INT_PATH}/intermediate/set-signed certificate=@"$scriptdir/data/${SMQ_VAULT_PKI_INT_FILE_NAME}.crt"
|
||||
fi
|
||||
}
|
||||
|
||||
vaultGenerateIntermediateCertificateBundle() {
|
||||
echo "Generate intermediate certificate bundle"
|
||||
cat "$scriptdir/data/${SMQ_VAULT_PKI_INT_FILE_NAME}.crt" "$scriptdir/data/${SMQ_VAULT_PKI_FILE_NAME}_ca.crt" \
|
||||
> "$scriptdir/data/${SMQ_VAULT_PKI_INT_FILE_NAME}_bundle.crt"
|
||||
}
|
||||
|
||||
vaultSetupIntermediateIssuingURLs() {
|
||||
echo "Setup URLs for CRL and issuing"
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} ${SMQ_VAULT_PKI_INT_PATH}/config/urls \
|
||||
issuing_certificates="{{cluster_aia_path}}/v1/${SMQ_VAULT_PKI_INT_PATH}/ca" \
|
||||
crl_distribution_points="{{cluster_aia_path}}/v1/${SMQ_VAULT_PKI_INT_PATH}/crl" \
|
||||
ocsp_servers="{{cluster_aia_path}}/v1/${SMQ_VAULT_PKI_INT_PATH}/ocsp" \
|
||||
enable_templating=true
|
||||
}
|
||||
|
||||
vaultSetupServerCertsRole() {
|
||||
if [ "$SKIP_SERVER_CERT" == "--skip-server-cert" ]; then
|
||||
echo "Skipping server certificate role"
|
||||
else
|
||||
echo "Setup Server certificate role"
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} ${SMQ_VAULT_PKI_INT_PATH}/roles/${SMQ_VAULT_PKI_INT_SERVER_CERTS_ROLE_NAME} \
|
||||
allow_subdomains=true \
|
||||
allow_any_name=true \
|
||||
max_ttl="4320h"
|
||||
fi
|
||||
}
|
||||
|
||||
vaultGenerateServerCertificate() {
|
||||
if [ "$SKIP_SERVER_CERT" == "--skip-server-cert" ]; then
|
||||
echo "Skipping generate server certificate"
|
||||
else
|
||||
echo "Generate server certificate"
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} -format=json ${SMQ_VAULT_PKI_INT_PATH}/issue/${SMQ_VAULT_PKI_INT_SERVER_CERTS_ROLE_NAME} \
|
||||
common_name="$server_name" ttl="4320h" \
|
||||
| tee >(jq -r .data.certificate >"$scriptdir/data/${server_name}.crt") \
|
||||
>(jq -r .data.private_key >"$scriptdir/data/${server_name}.key")
|
||||
fi
|
||||
}
|
||||
|
||||
vaultSetupClientCertsRole() {
|
||||
echo "Setup Client Certs role"
|
||||
vault write -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} ${SMQ_VAULT_PKI_INT_PATH}/roles/${SMQ_VAULT_PKI_INT_CLIENTS_CERTS_ROLE_NAME} \
|
||||
allow_subdomains=true \
|
||||
allow_any_name=true \
|
||||
max_ttl="2160h"
|
||||
}
|
||||
|
||||
vaultCleanupFiles() {
|
||||
if is_container_running "supermq-vault"; then
|
||||
docker exec supermq-vault sh -c 'rm -rf /vault/*.{crt,csr}'
|
||||
fi
|
||||
}
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "jq command could not be found, please install it and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$scriptdir/data"
|
||||
|
||||
vault login -namespace=${SMQ_VAULT_NAMESPACE} -address=${SMQ_VAULT_ADDR} ${SMQ_VAULT_TOKEN}
|
||||
|
||||
vaultEnablePKI
|
||||
vaultConfigPKIClusterPath
|
||||
vaultConfigPKICrl
|
||||
vaultAddRoleToSecret
|
||||
vaultGenerateRootCACertificate
|
||||
vaultSetupRootCAIssuingURLs
|
||||
vaultGenerateIntermediateCAPKI
|
||||
vaultConfigIntermediatePKIClusterPath
|
||||
vaultConfigIntermediatePKICrl
|
||||
vaultGenerateIntermediateCSR
|
||||
vaultSignIntermediateCSR
|
||||
vaultInjectIntermediateCertificate
|
||||
vaultGenerateIntermediateCertificateBundle
|
||||
vaultSetupIntermediateIssuingURLs
|
||||
vaultSetupServerCertsRole
|
||||
vaultGenerateServerCertificate
|
||||
vaultSetupClientCertsRole
|
||||
vaultCleanupFiles
|
||||
|
||||
exit 0
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
scriptdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
||||
|
||||
# default env file path
|
||||
env_file="docker/.env"
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
--env-file)
|
||||
if [[ -z "${2:-}" ]]; then
|
||||
echo "Error: --env-file requires a non-empty option argument."
|
||||
exit 1
|
||||
fi
|
||||
env_file="$2"
|
||||
if [[ ! -f "$env_file" ]]; then
|
||||
echo "Error: .env file not found at $env_file"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown parameter passed: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
readDotEnv() {
|
||||
set -o allexport
|
||||
source "$env_file"
|
||||
set +o allexport
|
||||
}
|
||||
|
||||
source "$scriptdir/vault_cmd.sh"
|
||||
|
||||
readDotEnv
|
||||
|
||||
vault operator unseal -address=${SMQ_VAULT_ADDR} ${SMQ_VAULT_UNSEAL_KEY_1}
|
||||
vault operator unseal -address=${SMQ_VAULT_ADDR} ${SMQ_VAULT_UNSEAL_KEY_2}
|
||||
vault operator unseal -address=${SMQ_VAULT_ADDR} ${SMQ_VAULT_UNSEAL_KEY_3}
|
||||
@@ -21,8 +21,6 @@ require (
|
||||
github.com/gofrs/uuid/v5 v5.3.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/hashicorp/vault/api v1.20.0
|
||||
github.com/hashicorp/vault/api/auth/approle v0.10.0
|
||||
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f
|
||||
github.com/jackc/pgtype v1.14.4
|
||||
github.com/jackc/pgx/v5 v5.7.5
|
||||
@@ -32,6 +30,7 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nats-io/nats.go v1.43.0
|
||||
github.com/oklog/ulid/v2 v2.1.1
|
||||
github.com/openbao/openbao/api/v2 v2.3.1
|
||||
github.com/ory/dockertest/v3 v3.12.0
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/plgd-dev/go-coap/v3 v3.4.0
|
||||
@@ -89,7 +88,7 @@ require (
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||
github.com/go-kit/log v0.2.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
@@ -106,7 +105,6 @@ require (
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||
@@ -126,7 +124,6 @@ require (
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/user v0.3.0 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
|
||||
@@ -115,8 +115,8 @@ github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8b
|
||||
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
|
||||
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU=
|
||||
github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
@@ -138,8 +138,8 @@ github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI6
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
|
||||
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
||||
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
@@ -160,6 +160,7 @@ github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaW
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
|
||||
@@ -188,8 +189,6 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
|
||||
@@ -198,10 +197,6 @@ github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9
|
||||
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
|
||||
github.com/hashicorp/vault/api v1.20.0 h1:KQMHElgudOsr+IbJgmbjHnCTxEpKs9LnozA1D3nozU4=
|
||||
github.com/hashicorp/vault/api v1.20.0/go.mod h1:GZ4pcjfzoOWpkJ3ijHNpEoAxKEsBJnVljyTe3jM2Sms=
|
||||
github.com/hashicorp/vault/api/auth/approle v0.10.0 h1:cFwz7NzhsC//3JMMEfYDKelSwZx7GhR4IdgJVgfKBgs=
|
||||
github.com/hashicorp/vault/api/auth/approle v0.10.0/go.mod h1:XJ++u6wVPOl7H2Wlb9zVvXungf5Ca1Agukq8rMwYogc=
|
||||
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8=
|
||||
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
@@ -315,8 +310,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
@@ -339,6 +332,8 @@ github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0
|
||||
github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=
|
||||
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
||||
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||
github.com/openbao/openbao/api/v2 v2.3.1 h1:+Ho5A1jWedZonDz+HDViSOXTieotUT6w7r2Q8Sc8GNM=
|
||||
github.com/openbao/openbao/api/v2 v2.3.1/go.mod h1:oEeWVQSz1LeJJGwwCiPzHX6seppRh8jYXaw6W6yYvao=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
|
||||
+24
-3
@@ -23,6 +23,8 @@ const (
|
||||
type Cert struct {
|
||||
SerialNumber string `json:"serial_number,omitempty"`
|
||||
Certificate string `json:"certificate,omitempty"`
|
||||
IssuingCA string `json:"issuing_ca,omitempty"`
|
||||
CAChain []string `json:"ca_chain,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Revoked bool `json:"revoked,omitempty"`
|
||||
ExpiryTime time.Time `json:"expiry_time,omitempty"`
|
||||
@@ -87,10 +89,29 @@ func (sdk mgSDK) ViewCertByClient(ctx context.Context, clientID, domainID, token
|
||||
return cs, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) RevokeCert(ctx context.Context, id, domainID, token string) (time.Time, errors.SDKError) {
|
||||
url := fmt.Sprintf("%s/%s/%s/%s", sdk.certsURL, domainID, certsEndpoint, id)
|
||||
func (sdk mgSDK) RevokeAllCerts(ctx context.Context, id, domainID, token string) (time.Time, errors.SDKError) {
|
||||
url := fmt.Sprintf("%s/%s/%s/%s/revoke-all", sdk.certsURL, domainID, certsEndpoint, id)
|
||||
|
||||
_, body, err := sdk.processRequest(ctx, http.MethodDelete, url, token, nil, nil, http.StatusOK)
|
||||
_, body, err := sdk.processRequest(ctx, http.MethodPost, url, token, nil, nil, http.StatusOK)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
var rcr revokeCertsRes
|
||||
if err := json.Unmarshal(body, &rcr); err != nil {
|
||||
return time.Time{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return rcr.RevocationTime, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) RevokeCert(ctx context.Context, certID, domainID, token string) (time.Time, errors.SDKError) {
|
||||
if certID == "" {
|
||||
return time.Time{}, errors.NewSDKError(apiutil.ErrMissingID)
|
||||
}
|
||||
url := fmt.Sprintf("%s/%s/%s/%s/revoke", sdk.certsURL, domainID, certsEndpoint, certID)
|
||||
|
||||
_, body, err := sdk.processRequest(ctx, http.MethodPost, url, token, nil, nil, http.StatusOK)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
+85
-5
@@ -39,7 +39,7 @@ var (
|
||||
cert, sdkCert = generateTestCerts(&testing.T{})
|
||||
defOffset uint64 = 0
|
||||
defLimit uint64 = 10
|
||||
defRevoke = "false"
|
||||
revoked = "all"
|
||||
)
|
||||
|
||||
func generateTestCerts(t *testing.T) (certs.Cert, sdk.Cert) {
|
||||
@@ -362,12 +362,12 @@ func TestViewCertByClient(t *testing.T) {
|
||||
tc.session = smqauthn.Session{DomainUserID: validID, UserID: validID, DomainID: validID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := svc.On("ListSerials", mock.Anything, tc.clientID, certs.PageMetadata{Revoked: defRevoke, Offset: defOffset, Limit: defLimit}).Return(tc.svcRes, tc.svcErr)
|
||||
svcCall := svc.On("ListSerials", mock.Anything, tc.clientID, certs.PageMetadata{Offset: defOffset, Limit: defLimit, Revoked: revoked}).Return(tc.svcRes, tc.svcErr)
|
||||
resp, err := mgsdk.ViewCertByClient(context.Background(), tc.clientID, tc.domainID, tc.token)
|
||||
assert.Equal(t, tc.err, err)
|
||||
if tc.err == nil {
|
||||
assert.Equal(t, viewCertClientRes, resp)
|
||||
ok := svcCall.Parent.AssertCalled(t, "ListSerials", mock.Anything, tc.clientID, certs.PageMetadata{Revoked: defRevoke, Offset: defOffset, Limit: defLimit})
|
||||
ok := svcCall.Parent.AssertCalled(t, "ListSerials", mock.Anything, tc.clientID, certs.PageMetadata{Offset: defOffset, Limit: defLimit, Revoked: revoked})
|
||||
assert.True(t, ok)
|
||||
}
|
||||
svcCall.Unset()
|
||||
@@ -376,7 +376,7 @@ func TestViewCertByClient(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevokeCert(t *testing.T) {
|
||||
func TestRevokeAllCert(t *testing.T) {
|
||||
ts, svc, auth := setupCerts()
|
||||
defer ts.Close()
|
||||
|
||||
@@ -452,7 +452,7 @@ func TestRevokeCert(t *testing.T) {
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := svc.On("RevokeCert", mock.Anything, tc.domainID, tc.token, tc.clientID).Return(tc.svcResp, tc.svcErr)
|
||||
resp, err := mgsdk.RevokeCert(context.Background(), tc.clientID, tc.domainID, tc.token)
|
||||
resp, err := mgsdk.RevokeAllCerts(context.Background(), tc.clientID, tc.domainID, tc.token)
|
||||
assert.Equal(t, tc.err, err)
|
||||
if err == nil {
|
||||
assert.NotEmpty(t, resp)
|
||||
@@ -464,3 +464,83 @@ func TestRevokeCert(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevokeCert(t *testing.T) {
|
||||
ts, svc, auth := setupCerts()
|
||||
defer ts.Close()
|
||||
|
||||
sdkConf := sdk.Config{
|
||||
CertsURL: ts.URL,
|
||||
MsgContentType: contentType,
|
||||
TLSVerification: false,
|
||||
}
|
||||
|
||||
mgsdk := sdk.NewSDK(sdkConf)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
certID string
|
||||
domainID string
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcResp certs.Revoke
|
||||
authenticateErr error
|
||||
svcErr error
|
||||
err errors.SDKError
|
||||
}{
|
||||
{
|
||||
desc: "revoke cert successfully",
|
||||
certID: serial,
|
||||
domainID: validID,
|
||||
token: validToken,
|
||||
svcResp: certs.Revoke{RevocationTime: time.Now()},
|
||||
svcErr: nil,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "revoke cert with invalid token",
|
||||
certID: serial,
|
||||
domainID: validID,
|
||||
token: invalidToken,
|
||||
svcResp: certs.Revoke{},
|
||||
authenticateErr: svcerr.ErrAuthentication,
|
||||
err: errors.NewSDKErrorWithStatus(svcerr.ErrAuthentication, http.StatusUnauthorized),
|
||||
},
|
||||
{
|
||||
desc: "revoke non-existing cert",
|
||||
certID: invalid,
|
||||
domainID: validID,
|
||||
token: validToken,
|
||||
svcResp: certs.Revoke{},
|
||||
svcErr: errors.Wrap(certs.ErrFailedCertRevocation, svcerr.ErrNotFound),
|
||||
err: errors.NewSDKErrorWithStatus(certs.ErrFailedCertRevocation, http.StatusNotFound),
|
||||
},
|
||||
{
|
||||
desc: "revoke cert with empty token",
|
||||
certID: serial,
|
||||
domainID: validID,
|
||||
token: "",
|
||||
svcResp: certs.Revoke{},
|
||||
svcErr: nil,
|
||||
err: errors.NewSDKErrorWithStatus(apiutil.ErrBearerToken, http.StatusUnauthorized),
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == valid {
|
||||
tc.session = smqauthn.Session{DomainUserID: validID, UserID: validID, DomainID: validID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := svc.On("RevokeBySerial", mock.Anything, tc.certID).Return(tc.svcResp, tc.svcErr)
|
||||
resp, err := mgsdk.RevokeCert(context.Background(), tc.certID, tc.domainID, tc.token)
|
||||
assert.Equal(t, tc.err, err)
|
||||
if err == nil {
|
||||
assert.NotEmpty(t, resp)
|
||||
ok := svcCall.Parent.AssertCalled(t, "RevokeBySerial", mock.Anything, tc.certID)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+88
-8
@@ -7666,12 +7666,12 @@ func (_c *SDK_ResetPasswordRequest_Call) RunAndReturn(run func(ctx context.Conte
|
||||
return _c
|
||||
}
|
||||
|
||||
// RevokeCert provides a mock function for the type SDK
|
||||
func (_mock *SDK) RevokeCert(ctx context.Context, clientID string, domainID string, token string) (time.Time, errors.SDKError) {
|
||||
// RevokeAllCerts provides a mock function for the type SDK
|
||||
func (_mock *SDK) RevokeAllCerts(ctx context.Context, clientID string, domainID string, token string) (time.Time, errors.SDKError) {
|
||||
ret := _mock.Called(ctx, clientID, domainID, token)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RevokeCert")
|
||||
panic("no return value specified for RevokeAllCerts")
|
||||
}
|
||||
|
||||
var r0 time.Time
|
||||
@@ -7694,6 +7694,86 @@ func (_mock *SDK) RevokeCert(ctx context.Context, clientID string, domainID stri
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SDK_RevokeAllCerts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RevokeAllCerts'
|
||||
type SDK_RevokeAllCerts_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RevokeAllCerts is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - clientID string
|
||||
// - domainID string
|
||||
// - token string
|
||||
func (_e *SDK_Expecter) RevokeAllCerts(ctx interface{}, clientID interface{}, domainID interface{}, token interface{}) *SDK_RevokeAllCerts_Call {
|
||||
return &SDK_RevokeAllCerts_Call{Call: _e.mock.On("RevokeAllCerts", ctx, clientID, domainID, token)}
|
||||
}
|
||||
|
||||
func (_c *SDK_RevokeAllCerts_Call) Run(run func(ctx context.Context, clientID string, domainID string, token string)) *SDK_RevokeAllCerts_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
var arg3 string
|
||||
if args[3] != nil {
|
||||
arg3 = args[3].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
arg3,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_RevokeAllCerts_Call) Return(time1 time.Time, sDKError errors.SDKError) *SDK_RevokeAllCerts_Call {
|
||||
_c.Call.Return(time1, sDKError)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_RevokeAllCerts_Call) RunAndReturn(run func(ctx context.Context, clientID string, domainID string, token string) (time.Time, errors.SDKError)) *SDK_RevokeAllCerts_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RevokeCert provides a mock function for the type SDK
|
||||
func (_mock *SDK) RevokeCert(ctx context.Context, certID string, domainID string, token string) (time.Time, errors.SDKError) {
|
||||
ret := _mock.Called(ctx, certID, domainID, token)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RevokeCert")
|
||||
}
|
||||
|
||||
var r0 time.Time
|
||||
var r1 errors.SDKError
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) (time.Time, errors.SDKError)); ok {
|
||||
return returnFunc(ctx, certID, domainID, token)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) time.Time); ok {
|
||||
r0 = returnFunc(ctx, certID, domainID, token)
|
||||
} else {
|
||||
r0 = ret.Get(0).(time.Time)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string) errors.SDKError); ok {
|
||||
r1 = returnFunc(ctx, certID, domainID, token)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(errors.SDKError)
|
||||
}
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SDK_RevokeCert_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RevokeCert'
|
||||
type SDK_RevokeCert_Call struct {
|
||||
*mock.Call
|
||||
@@ -7701,14 +7781,14 @@ type SDK_RevokeCert_Call struct {
|
||||
|
||||
// RevokeCert is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - clientID string
|
||||
// - certID string
|
||||
// - domainID string
|
||||
// - token string
|
||||
func (_e *SDK_Expecter) RevokeCert(ctx interface{}, clientID interface{}, domainID interface{}, token interface{}) *SDK_RevokeCert_Call {
|
||||
return &SDK_RevokeCert_Call{Call: _e.mock.On("RevokeCert", ctx, clientID, domainID, token)}
|
||||
func (_e *SDK_Expecter) RevokeCert(ctx interface{}, certID interface{}, domainID interface{}, token interface{}) *SDK_RevokeCert_Call {
|
||||
return &SDK_RevokeCert_Call{Call: _e.mock.On("RevokeCert", ctx, certID, domainID, token)}
|
||||
}
|
||||
|
||||
func (_c *SDK_RevokeCert_Call) Run(run func(ctx context.Context, clientID string, domainID string, token string)) *SDK_RevokeCert_Call {
|
||||
func (_c *SDK_RevokeCert_Call) Run(run func(ctx context.Context, certID string, domainID string, token string)) *SDK_RevokeCert_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
@@ -7741,7 +7821,7 @@ func (_c *SDK_RevokeCert_Call) Return(time1 time.Time, sDKError errors.SDKError)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_RevokeCert_Call) RunAndReturn(run func(ctx context.Context, clientID string, domainID string, token string) (time.Time, errors.SDKError)) *SDK_RevokeCert_Call {
|
||||
func (_c *SDK_RevokeCert_Call) RunAndReturn(run func(ctx context.Context, certID string, domainID string, token string) (time.Time, errors.SDKError)) *SDK_RevokeCert_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
+10
-3
@@ -1065,12 +1065,19 @@ type SDK interface {
|
||||
// fmt.Println(cserial)
|
||||
ViewCertByClient(ctx context.Context, clientID, domainID, token string) (CertSerials, errors.SDKError)
|
||||
|
||||
// RevokeCert revokes certificate for client with clientID
|
||||
// RevokeAllCerts revokes all certificates for client with clientID
|
||||
//
|
||||
// example:
|
||||
// tm, _ := sdk.RevokeCert("clientID", "domainID", "token")
|
||||
// tm, _ := sdk.RevokeAllCerts("clientID", "domainID", "token")
|
||||
// fmt.Println(tm)
|
||||
RevokeCert(ctx context.Context, clientID, domainID, token string) (time.Time, errors.SDKError)
|
||||
RevokeAllCerts(ctx context.Context, clientID, domainID, token string) (time.Time, errors.SDKError)
|
||||
|
||||
//RevokeCert revokes a certificate with given certID.
|
||||
//
|
||||
// example:
|
||||
// tm, _ := sdk.RevokeCert("certID", "domainID", "token")
|
||||
// fmt.Println(tm)
|
||||
RevokeCert(ctx context.Context, certID, domainID, token string) (time.Time, errors.SDKError)
|
||||
|
||||
// CreateDomain creates new domain and returns its details.
|
||||
//
|
||||
|
||||
@@ -53,7 +53,7 @@ packages:
|
||||
structname: "SDK"
|
||||
filename: "sdk.go"
|
||||
|
||||
github.com/absmach/supermq/certs/pki/amcerts:
|
||||
github.com/absmach/supermq/certs/pki/openbao:
|
||||
interfaces:
|
||||
Agent:
|
||||
config:
|
||||
@@ -72,6 +72,7 @@ packages:
|
||||
github.com/absmach/supermq/certs:
|
||||
interfaces:
|
||||
Service:
|
||||
Repository:
|
||||
github.com/absmach/supermq/channels:
|
||||
interfaces:
|
||||
Cache:
|
||||
|
||||
Reference in New Issue
Block a user