Xác thực OpenStack và Token Management
Giới thiệu
Trong bài này, chúng ta sẽ tìm hiểu về OpenStack Keystone authentication và cách quản lý scoped tokens cho các requests đến OpenStack APIs.
OpenStack Keystone Authentication
Keystone là gì?
Keystone là OpenStack Identity Service, cung cấp:
- Authentication (xác thực người dùng)
- Authorization (phân quyền)
- Service catalog (danh sách services và endpoints)
- Token management (quản lý tokens)
Authentication Flow
1. Client → Keystone: POST /auth/tokens
Body: {
auth: {
identity: { methods: ['password'], password: { user: {...} } },
scope: { project: { id: projectId } }
}
}
2. Keystone → Client: Response
Headers: {
'X-Subject-Token': '<scoped-token>'
}
Body: { token: {...} }
3. Client → OpenStack Services (Nova, Glance):
Headers: {
'X-Auth-Token': '<scoped-token>'
}Scoped Token
Token Types
OpenStack hỗ trợ 2 loại tokens:
Unscoped Token
- Không gắn với project cụ thể
- Chỉ dùng để list projects
Scoped Token (Project-scoped)
- Gắn với một project cụ thể
- Dùng để access resources trong project đó
- Chúng ta sử dụng loại này
Scoped Token Structure
typescript
{
auth: {
identity: {
methods: ['password'],
password: {
user: {
name: 'username',
password: 'password',
domain: { name: 'Default' }
}
}
},
scope: {
project: {
id: 'project-id' // hoặc { name: 'project-name', domain: {...} }
}
}
}
}Implementation trong Portal
Environment Variables
Mỗi location (HCM/HNI) có bộ environment variables riêng:
bash
# HCM (Hồ Chí Minh)
OPENSTACK_ENDPOINT_HCM=https://keystone-hcm.inet.vn:5000
OPENSTACK_USERNAME_HCM=portal-user-hcm
OPENSTACK_PASSWORD_HCM=secret-password-hcm
OPENSTACK_PROJECT_ID_HCM=project-id-hcm
OPENSTACK_USER_DOMAIN_NAME_HCM=Default
# HNI (Hà Nội)
OPENSTACK_ENDPOINT_HN=https://keystone-hni.inet.vn:5000
OPENSTACK_USERNAME_HN=portal-user-hni
OPENSTACK_PASSWORD_HN=secret-password-hni
OPENSTACK_PROJECT_ID_HN=project-id-hni
OPENSTACK_USER_DOMAIN_NAME_HN=DefaultOpenStack Environment Configuration
typescript
export type OpenStackEnv = {
authUrl: string; // Base endpoint + /identity/v3
baseEndpoint: string; // Base endpoint (e.g., https://keystone.inet.vn:5000)
username: string;
password: string;
userDomainName?: string;
projectId?: string;
projectName?: string;
projectDomainName?: string;
region: string;
interfaceType: 'public' | 'internal' | 'admin';
};
export function getOpenStackEnv(location?: string): OpenStackEnv {
// Normalize: undefined or empty defaults to HCM
const normalizedLocation = location && location !== '' ? location : 'HCM';
const isHanoi = normalizedLocation === 'HNI';
// Get base endpoint from env
const baseEndpoint = isHanoi
? (process.env.OPENSTACK_ENDPOINT_HN || process.env.OPENSTACK_ENDPOINT || '')
: (process.env.OPENSTACK_ENDPOINT_HCM || process.env.OPENSTACK_ENDPOINT || '');
// Build authUrl from baseEndpoint
const authUrl = baseEndpoint ? `${baseEndpoint.replace(/\/$/, '')}/identity/v3` : '';
// ... other configs (username, password, projectId, etc.)
return {
authUrl,
baseEndpoint,
username,
password,
// ... other fields
};
}Get Scoped Token Function
typescript
async function getScopedToken(location?: string) {
const env = getOpenStackEnv(location);
const payload: any = {
auth: {
identity: {
methods: ['password'],
password: {
user: {
name: env.username,
password: env.password,
domain: env.userDomainName
? { name: env.userDomainName }
: { name: 'Default' },
},
},
},
scope: env.projectId
? { project: { id: env.projectId } }
: {
project: {
name: env.projectName,
domain: env.projectDomainName
? { name: env.projectDomainName }
: undefined
}
},
},
};
const url = `${env.authUrl.replace(/\/$/, '')}/auth/tokens`;
const client = createAxios();
const res = await client.post(url, payload);
const token = String(res.headers['x-subject-token'] || res.headers['X-Subject-Token'] || '');
if (!token) throw new Error('Auth failed: missing X-Subject-Token');
return { token };
}Token Usage trong API Calls
typescript
export const createServerSnapshotOpenStack = async function (params: {
serverId: string;
snapshotName: string;
location?: string;
}) {
// 1. Get scoped token
const { token } = await getScopedToken(location);
// 2. Build endpoint
const computeBase = buildEndpoint('compute', location);
// 3. Create axios client with token
const nova = createAxios(computeBase, { 'X-Auth-Token': token });
// 4. Call Nova API
const actionRes = await nova.post(
`/servers/${encodeURIComponent(serverId)}/action`,
{
createImage: {
name: snapshotName,
metadata: { source: 'portal-manual' }
}
}
);
// ...
};Token Lifecycle
Token Expiration
- Default expiration: Thường là 1 giờ
- Không cache tokens: Mỗi request lấy token mới (stateless)
- Lý do: Đơn giản hóa, không cần refresh logic
Stateless Token Pattern
typescript
// ✅ DO: Get token per request (stateless)
async function callOpenStackAPI(location?: string) {
const { token } = await getScopedToken(location);
const client = createAxios(endpoint, { 'X-Auth-Token': token });
return await client.get('/some-endpoint');
}
// ❌ DON'T: Cache tokens (có thể expired)
let cachedToken: string | null = null;
async function callOpenStackAPI() {
if (!cachedToken) {
cachedToken = await getScopedToken();
}
// Token có thể đã expired
}Location-Based Authentication
Normalize Location
typescript
function normalizeLocation(location?: string): string {
if (!location || location === '') return 'HCM';
if (location === 'HNI') return 'HNI';
return 'HCM'; // Default to HCM
}Location Routing
typescript
export function getOpenStackEnv(location?: string): OpenStackEnv {
const normalizedLocation = normalizeLocation(location);
const isHanoi = normalizedLocation === 'HNI';
// Get config based on location
const baseEndpoint = isHanoi
? process.env.OPENSTACK_ENDPOINT_HN
: process.env.OPENSTACK_ENDPOINT_HCM;
const username = isHanoi
? process.env.OPENSTACK_USERNAME_HN
: process.env.OPENSTACK_USERNAME_HCM;
// ... other configs
}Usage Example
typescript
// HCM server
const { token } = await getScopedToken('HCM');
// Uses: OPENSTACK_ENDPOINT_HCM, OPENSTACK_USERNAME_HCM, etc.
// HNI server
const { token } = await getScopedToken('HNI');
// Uses: OPENSTACK_ENDPOINT_HN, OPENSTACK_USERNAME_HN, etc.
// Default to HCM
const { token } = await getScopedToken();
// Uses: OPENSTACK_ENDPOINT_HCM (default)Error Handling
Common Errors
1. Missing Environment Variables
typescript
if (!baseEndpoint || !username || !password || (!projectId && !projectName)) {
throw new Error(
`Missing env for ${isHanoi ? 'Hà Nội' : 'HCM'}: ` +
`require OPENSTACK_ENDPOINT, OPENSTACK_USERNAME, ` +
`OPENSTACK_PASSWORD and OPENSTACK_PROJECT_ID or OPENSTACK_PROJECT_NAME`
);
}2. Authentication Failed
typescript
const token = String(res.headers['x-subject-token'] || '');
if (!token) {
throw new Error('Auth failed: missing X-Subject-Token');
}3. Invalid Credentials
OpenStack sẽ trả về HTTP 401 với error message. Cần log và handle:
typescript
try {
const res = await client.post(url, payload);
// ...
} catch (error: any) {
if (error.response?.status === 401) {
logger.error('❌ OpenStack authentication failed - invalid credentials');
throw new Error('Invalid OpenStack credentials');
}
throw error;
}Best Practices
1. Always Normalize Location
typescript
// ✅ DO
const normalizedLocation = normalizeLocation(location);
const { token } = await getScopedToken(normalizedLocation);
// ❌ DON'T
const { token } = await getScopedToken(location); // Location có thể undefined2. Handle Token Errors Gracefully
typescript
try {
const { token } = await getScopedToken(location);
// ...
} catch (error: any) {
logger.error(`❌ Failed to get OpenStack token: ${error.message}`);
throw new Error('Failed to authenticate with OpenStack');
}3. Use Environment Variables Correctly
typescript
// ✅ DO: Use location-specific env vars
const baseEndpoint = isHanoi
? process.env.OPENSTACK_ENDPOINT_HN
: process.env.OPENSTACK_ENDPOINT_HCM;
// ❌ DON'T: Mix location configs
const baseEndpoint = process.env.OPENSTACK_ENDPOINT; // Generic, no location distinction4. Log Authentication Attempts
typescript
logger.info(`🔐 Getting OpenStack token for location: ${normalizedLocation}`);
const { token } = await getScopedToken(normalizedLocation);
logger.info(`✅ OpenStack token obtained (length: ${token.length})`);Testing Authentication
Unit Test Example
typescript
describe('getScopedToken', () => {
it('should get token for HCM location', async () => {
process.env.OPENSTACK_ENDPOINT_HCM = 'https://keystone-hcm.inet.vn:5000';
process.env.OPENSTACK_USERNAME_HCM = 'test-user';
process.env.OPENSTACK_PASSWORD_HCM = 'test-password';
process.env.OPENSTACK_PROJECT_ID_HCM = 'test-project-id';
const { token } = await getScopedToken('HCM');
expect(token).toBeDefined();
expect(token.length).toBeGreaterThan(0);
});
it('should default to HCM if location is empty', async () => {
const { token } = await getScopedToken('');
// Should use HCM config
expect(token).toBeDefined();
});
});Manual Testing
bash
# Test HCM authentication
curl -X POST https://keystone-hcm.inet.vn:5000/identity/v3/auth/tokens \
-H "Content-Type: application/json" \
-d '{
"auth": {
"identity": {
"methods": ["password"],
"password": {
"user": {
"name": "username",
"password": "password",
"domain": { "name": "Default" }
}
}
},
"scope": {
"project": {
"id": "project-id"
}
}
}
}'
# Response headers should contain X-Subject-TokenSummary
Key Points
Scoped Token Pattern
- Mỗi request lấy token mới (stateless)
- Token trong header
X-Auth-Token - Token có expiration (thường 1 giờ)
Location-Based Config
- Mỗi location (HCM/HNI) có env vars riêng
- Normalize location trước khi dùng
- Default to HCM nếu không chỉ định
Error Handling
- Check missing env vars
- Handle authentication failures
- Log errors để debug
Best Practices
- Always normalize location
- Use location-specific env vars
- Log authentication attempts
- Handle errors gracefully
Next Steps
Trong bài tiếp theo, chúng ta sẽ tìm hiểu về:
- Snapshot & Backup - Tạo snapshot và backup với Nova/Glance APIs
Last Updated: 2025-01-25
Previous: 01. OpenStack Overview
Next: 03. Snapshot & Backup