Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/bruno-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ jobs:
echo "Running users & tokens integration tests..."
cd bruno/users_tokens_integration_tests && bru run --env local

- name: Run Certificates Integration Tests
run: |
echo "Running certificates integration tests..."
cd bruno/certificates_integration_tests && bru run --env local

- name: Stop server
if: always()
run: |
Expand Down
154 changes: 146 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ Self-hosted secrets management with web UI and REST API.

## Features

- Web UI with dark mode
- SQLite storage (single binary, no dependencies)
- Multi-user with JWT authentication
- API tokens with pattern-based permissions
- Audit logging
- 🔐 Web UI with dark mode

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot remove emojis. Keep it professional.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove all emojis from README Features section.

- 💾 SQLite storage (single binary, no dependencies)
- 👥 Multi-user with JWT authentication
- 🔑 API tokens with pattern-based permissions
- 📜 Audit logging
- 🔏 Certificate & Key Management (RSA, ECDSA, ED25519)
- Generate key pairs
- Import/Export certificates (PEM format)
- Create self-signed & CA-signed certificates
- Certificate verification & validation

## Screenshots

Expand Down Expand Up @@ -109,9 +114,138 @@ Then use `Authorization: Bearer <jwt>` for:
| POST | `/api/secrets` | Create secret |
| PUT | `/api/secrets?key=` | Update secret |
| DELETE | `/api/secrets?key=` | Delete secret |
| GET/POST/PUT/DELETE | `/api/users` | Manage users |
| GET/POST/PUT/DELETE | `/api/tokens` | Manage tokens |
| GET/POST/PUT/DELETE | `/api/permissions` | Manage permissions |
| GET/POST/PUT/DELETE | `/api/users` | Manage users |
| GET/POST/PUT/DELETE | `/api/tokens` | Manage tokens |
| GET/POST/PUT/DELETE | `/api/permissions` | Manage permissions |
| GET/POST/DELETE | `/api/certificates` | Manage certificates |

## Certificate Management

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Certificate management should be much shorter description and preferably at the bottom. It's a niche feature

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will move Certificate Management section to the bottom of README with a much shorter description (1-2 paragraphs max).


The secrets manager includes comprehensive certificate and key management capabilities:

### Generate Key Pairs

Generate RSA, ECDSA, or ED25519 key pairs:

```bash
POST /api/certificates/generate-keypair
Authorization: Bearer <jwt>
Content-Type: application/json

{
"name": "my-keypair",
"algorithm": "RSA", // "RSA", "ECDSA", or "ED25519"
"key_size": 2048 // RSA: 2048, 3072, 4096; ECDSA: 256, 384, 521
}
```

### Generate Certificates

Create self-signed or CA-signed X.509 certificates:

```bash
POST /api/certificates/generate-certificate
Authorization: Bearer <jwt>
Content-Type: application/json

{
"name": "my-cert",
"private_key_name": "my-keypair-private",
"subject": {
"common_name": "example.com",
"organization": "My Organization",
"country": "US"
},
"validity_days": 365,
"is_ca": false,
"dns_names": ["example.com", "www.example.com"],
"signing_cert_name": "ca-cert" // Optional: for CA-signed certs
}
```

### Import/Export Certificates

```bash
# Import a certificate
POST /api/certificates/import
Authorization: Bearer <jwt>
Content-Type: application/json

{
"name": "imported-cert",
"cert_type": "certificate", // "private_key", "public_key", "certificate", "ca_certificate"
"pem_data": "-----BEGIN CERTIFICATE-----\n..."
}

# Export a certificate
GET /api/certificates/{name}/export
Authorization: Bearer <jwt>
```

### Verify Certificates

Verify certificate validity and signatures:

```bash
POST /api/certificates/verify
Authorization: Bearer <jwt>
Content-Type: application/json

{
"certificate_name": "my-cert",
"ca_cert_name": "ca-cert" // Optional: verify against specific CA
}
```

### Go SDK - Certificate Management

```go
// Generate RSA key pair
keyPair, err := client.GenerateKeyPair(secretssdk.GenerateKeyPairRequest{
Name: "my-rsa-key",
Algorithm: "RSA",
KeySize: ptrInt(2048),
})

// Generate self-signed certificate
cert, err := client.GenerateCertificate(secretssdk.GenerateCertificateRequest{
Name: "my-cert",
PrivateKeyName: "my-rsa-key-private",
Subject: secretssdk.Subject{
CommonName: "example.com",
Organization: "My Org",
Country: "US",
},
ValidityDays: 365,
IsCA: false,
DNSNames: []string{"example.com", "*.example.com"},
})

// Verify certificate
result, err := client.VerifyCertificate(secretssdk.VerifyCertificateRequest{
CertificateName: "my-cert",
})

// Export certificate
exported, err := client.ExportCertificate("my-cert")
fmt.Println(exported.PemData)
```

### List & Manage Certificates

```bash
# List all certificates
GET /api/certificates
Authorization: Bearer <jwt>

# Get specific certificate
GET /api/certificates/{name}
Authorization: Bearer <jwt>

# Delete certificate
DELETE /api/certificates/{name}
Authorization: Bearer <jwt>
```

## Pattern Matching

Expand All @@ -138,5 +272,9 @@ cd web && yarn dev # Runs on localhost:5173, proxies API to :7770
Integration tests (requires [Bruno CLI](https://www.usebruno.com/)):

```bash
# Run all test collections
cd bruno/auth_integration_tests && bru run --env local
cd bruno/secrets_integration_tests && bru run --env local
cd bruno/users_tokens_integration_tests && bru run --env local
cd bruno/certificates_integration_tests && bru run --env local
```
38 changes: 38 additions & 0 deletions bruno/certificates_integration_tests/01 - Login as admin.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
meta {
name: 01 - Login as admin
type: http
seq: 1
}

post {
url: {{burl}}/login
body: json
auth: none
}

body:json {
{
"username": "admin",
"password": "{{admin_password}}"
}
}

script:post-response {
if (res.body.data && res.body.data.token) {
bru.setEnvVar("admin_token", res.body.data.token);
}
}

assert {
res.status: eq 200
res.body.success: eq true
res.body.data.token: isDefined
}

tests {
test("Login should succeed with correct credentials", function() {
expect(res.getStatus()).to.equal(200);
expect(res.getBody().success).to.be.true;
expect(res.getBody().data.token).to.be.a('string');
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
meta {
name: 02 - Generate RSA key pair
type: http
seq: 2
}

post {
url: {{burl}}/api/certificates/generate-keypair
body: json
auth: inherit
}

headers {
Authorization: Bearer {{admin_token}}
Content-Type: application/json
}

body:json {
{
"name": "test-rsa",
"algorithm": "RSA",
"key_size": 2048
}
}

script:post-response {
if (res.body.data && res.body.data.private_key) {
bru.setEnvVar("rsa_private_key_name", res.body.data.private_key.name);
bru.setEnvVar("rsa_public_key_name", res.body.data.public_key.name);
}
}

assert {
res.status: eq 200
res.body.success: eq true
res.body.data.private_key.name: eq test-rsa-private
res.body.data.public_key.name: eq test-rsa-public
res.body.data.private_key.algorithm: eq RSA
res.body.data.private_key.cert_type: eq private_key
res.body.data.public_key.cert_type: eq public_key
}

tests {
test("RSA key pair generation should succeed", function() {
expect(res.getStatus()).to.equal(200);
expect(res.getBody().data.private_key.pem_data).to.contain("RSA PRIVATE KEY");
expect(res.getBody().data.public_key.pem_data).to.contain("PUBLIC KEY");
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
meta {
name: 03 - Generate ECDSA key pair
type: http
seq: 3
}

post {
url: {{burl}}/api/certificates/generate-keypair
body: json
auth: inherit
}

headers {
Authorization: Bearer {{admin_token}}
Content-Type: application/json
}

body:json {
{
"name": "test-ecdsa",
"algorithm": "ECDSA",
"key_size": 256
}
}

script:post-response {
if (res.body.data && res.body.data.private_key) {
bru.setEnvVar("ecdsa_private_key_name", res.body.data.private_key.name);
bru.setEnvVar("ecdsa_public_key_name", res.body.data.public_key.name);
}
}

assert {
res.status: eq 200
res.body.success: eq true
res.body.data.private_key.name: eq test-ecdsa-private
res.body.data.public_key.name: eq test-ecdsa-public
res.body.data.private_key.algorithm: eq ECDSA
}

tests {
test("ECDSA key pair generation should succeed", function() {
expect(res.getStatus()).to.equal(200);
expect(res.getBody().data.private_key.pem_data).to.contain("EC PRIVATE KEY");
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
meta {
name: 04 - Generate ED25519 key pair
type: http
seq: 4
}

post {
url: {{burl}}/api/certificates/generate-keypair
body: json
auth: inherit
}

headers {
Authorization: Bearer {{admin_token}}
Content-Type: application/json
}

body:json {
{
"name": "test-ed25519",
"algorithm": "ED25519"
}
}

script:post-response {
if (res.body.data && res.body.data.private_key) {
bru.setEnvVar("ed25519_private_key_name", res.body.data.private_key.name);
}
}

assert {
res.status: eq 200
res.body.success: eq true
res.body.data.private_key.name: eq test-ed25519-private
res.body.data.private_key.algorithm: eq ED25519
}

tests {
test("ED25519 key pair generation should succeed", function() {
expect(res.getStatus()).to.equal(200);
expect(res.getBody().data.private_key.pem_data).to.contain("PRIVATE KEY");
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
meta {
name: 05 - List all certificates
type: http
seq: 5
}

get {
url: {{burl}}/api/certificates
body: none
auth: inherit
}

headers {
Authorization: Bearer {{admin_token}}
}

assert {
res.status: eq 200
res.body.success: eq true
}

tests {
test("Should list all certificates", function() {
expect(res.getStatus()).to.equal(200);
expect(res.getBody().data).to.be.an('array');
expect(res.getBody().data.length).to.be.at.least(6);
});
}
Loading
Loading