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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/backend-docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v6

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/frontend-docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v6

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

Expand Down
4 changes: 4 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,7 @@ KK_AUTHENTIK_CLIENT_ID=krakenkey-backend
KK_AUTHENTIK_CLIENT_SECRET=your_authentik_client_secret_here
KK_AUTHENTIK_REDIRECT_URI=https://api-dev.krakenkey.io/auth/callback
KK_AUTHENTIK_POST_ENROLLMENT_REDIRECT=https://api-dev.krakenkey.io/auth/login

## HMAC Secret (API key hashing) ##
# Generate with: openssl rand -hex 32
KK_HMAC_SECRET=your_hmac_secret_here
16 changes: 7 additions & 9 deletions backend/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { getRepositoryToken } from '@nestjs/typeorm';
import { createHmac } from 'crypto';
import { scryptSync } from 'crypto';
import { NotFoundException, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UserApiKey } from './entities/user-api-key.entity';
Expand Down Expand Up @@ -323,7 +323,7 @@ describe('AuthService', () => {
expect(result.apiKey).toMatch(/^kk_/);
});

it('stores the HMAC-SHA256 hash, not the raw key', async () => {
it('stores the scrypt hash, not the raw key', async () => {
let capturedHash = '';
mockUserApiKeyRepo.create.mockImplementation(
({ hash }: { hash: string }) => {
Expand All @@ -334,9 +334,9 @@ describe('AuthService', () => {
mockUserApiKeyRepo.save.mockResolvedValue({});

const result = await service.createApiKey('user-1', 'k');
const expectedHash = createHmac('sha256', HMAC_SECRET)
.update(result.apiKey)
.digest('hex');
const expectedHash = scryptSync(result.apiKey, HMAC_SECRET, 64).toString(
'hex',
);
expect(capturedHash).toBe(expectedHash);
});

Expand Down Expand Up @@ -364,15 +364,13 @@ describe('AuthService', () => {
// validateApiKey
// ---------------------------------------------------------------------------
describe('validateApiKey', () => {
it('looks up the HMAC-SHA256 hash of the raw key', async () => {
it('looks up the scrypt hash of the raw key', async () => {
mockUserApiKeyRepo.findOne.mockResolvedValue(null);
const rawKey = 'kk_test_raw_key';

await service.validateApiKey(rawKey);

const expectedHash = createHmac('sha256', HMAC_SECRET)
.update(rawKey)
.digest('hex');
const expectedHash = scryptSync(rawKey, HMAC_SECRET, 64).toString('hex');
expect(mockUserApiKeyRepo.findOne).toHaveBeenCalledWith({
where: { hash: expectedHash },
relations: ['user'],
Expand Down
23 changes: 19 additions & 4 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { In, Repository } from 'typeorm';
import { UserApiKey } from './entities/user-api-key.entity';
import { ServiceApiKey } from './entities/service-api-key.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { createHmac, randomBytes } from 'crypto';
import { scryptSync, randomBytes } from 'crypto';
import { User } from '../users/entities/user.entity';
import { Domain } from '../domains/entities/domain.entity';
import { TlsCrt } from '../certs/tls/entities/tls-crt.entity';
Expand Down Expand Up @@ -46,19 +46,28 @@ export class AuthService implements OnModuleInit {
) {}

async onModuleInit() {
const secret = this.config.get<string>('KK_HMAC_SECRET');
if (!secret) {
this.logger.error(
'KK_HMAC_SECRET is not set — API key creation and validation will fail. ' +
'Add the secret to your environment before using key-based auth.',
);
}
await this.seedServiceKey();
}

/**
* HMAC-SHA256 key hashing with a server-side secret.
* scrypt key hashing with a server-side secret as salt.
* Prevents offline key verification if the DB is compromised without the secret.
*/
private hashKey(raw: string): string {
const secret = this.config.get<string>('KK_HMAC_SECRET');
if (!secret) {
throw new Error('KK_HMAC_SECRET must be set');
throw new Error(
'KK_HMAC_SECRET must be set to use key-based authentication',
);
}
return createHmac('sha256', secret).update(raw).digest('hex');
return scryptSync(raw, secret, 64).toString('hex');
}

/**
Expand All @@ -69,6 +78,12 @@ export class AuthService implements OnModuleInit {
const rawKey = this.config.get<string>('KK_PROBE_API_KEY');
if (!rawKey) return;

const secret = this.config.get<string>('KK_HMAC_SECRET');
if (!secret) {
this.logger.warn('Skipping service key seed — KK_HMAC_SECRET is not set');
return;
}

const hash = this.hashKey(rawKey);
const existing = await this.serviceApiKeyRepo.findOne({ where: { hash } });
if (existing) return;
Expand Down
Loading