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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ Controller v3.8 is a **greenfield** release aligned with **Edgelet**. There is *
- Central local CAs (`default-router-local-ca`, `default-nats-local-ca`) are ensured on first agent provision (or via operator direct import before first agent), not at Controller boot — allows custom local CAs before agent deployment.
- Fog teardown drops obsolete per-fog **`nats-local-ca-*`** and **`router-local-ca-*`** secret names from cleanup lists.
- OIDC discovery with **`AUTH_INSECURE_ALLOW_HTTP`** uses the supported `openid-client` insecure-request hook for local **`http://`** issuers.
- Embedded OAuth BFF builds the in-process issuer client from local metadata and trusts listener TLS material (**`TLS_PATH_*`** / **`TLS_BASE64_*`**) for token exchange — fixes **`fetch failed`** on **`GET /api/v3/user/oauth/authorize`** with self-signed HTTPS certs without **`NODE_EXTRA_CA_CERTS`**.
- Provisioning key and **`GET /api/v3/agent/cert`** derive **`caCert`** from listener TLS material (**`TLS_PATH_*`** / **`TLS_BASE64_*`**) via shared **`tls-config`** — always base64-encoded for Edgelet trust store; fixes empty **`caCert`** when legacy **`SSL_CERT`** / **`INTERMEDIATE_CERT`** were unset.
- Config keys **`auth.bootstrap.adminUsername`** / **`adminPassword`** renamed to **`auth.bootstrap.username`** / **`password`** (**`OIDC_BOOTSTRAP_ADMIN_*`** env vars unchanged).
- Microservice create/update **strips** user-supplied **`serviceAccount`** volume mappings instead of rejecting them — allows GET → PATCH round-trips; system still injects the canonical binding.
- OIDC middleware **skips Bearer validation** on public catalog routes (e.g. **`GET /api/v3/status`**, **`GET /api/v3/architectures/`**, OAuth BFF) so agent/controller tokens on health checks no longer spam JWKS warnings.
Expand Down
3 changes: 3 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7532,6 +7532,9 @@ components:
type: string
expirationTime:
type: number
caCert:
type: string
description: Base64-encoded PEM of the controller CA for Edgelet trust store; empty when a public CA is used
AgentProvisioningRequest:
type: object
required:
Expand Down
30 changes: 29 additions & 1 deletion src/config/embedded-oidc.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,38 @@ function resetEmbeddedIssuerForTests () {
providerInstance = null
}

function getEmbeddedIssuerMetadata (issuerUrl) {
const base = issuerUrl.replace(/\/$/, '')
return {
issuer: base,
authorization_endpoint: `${base}/auth`,
token_endpoint: `${base}/token`,
jwks_uri: `${base}/jwks`,
userinfo_endpoint: `${base}/me`,
end_session_endpoint: `${base}/session/end`,
revocation_endpoint: `${base}/revoke`,
pushed_authorization_request_endpoint: `${base}/request`,
response_types_supported: ['code id_token', 'code', 'id_token', 'none'],
grant_types_supported: ['implicit', 'authorization_code'],
code_challenge_methods_supported: ['S256'],
token_endpoint_auth_methods_supported: [
'client_secret_basic',
'client_secret_jwt',
'client_secret_post',
'private_key_jwt',
'none'
],
id_token_signing_alg_values_supported: ['RS256'],
subject_types_supported: ['public'],
scopes_supported: ['openid', 'profile', 'email', 'groups']
}
}

module.exports = {
initEmbeddedIssuer,
getEmbeddedProvider,
resetEmbeddedIssuerForTests,
getOauthInteractionPath,
buildInteractionRedirectUrl
buildInteractionRedirectUrl,
getEmbeddedIssuerMetadata
}
37 changes: 37 additions & 0 deletions src/config/oidc-fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const { Agent, fetch: undiciFetch } = require('undici')
const { getListenerTlsMaterial, getListenerTrustCaBuffers } = require('../utils/tls-config')

let cachedFetch

function resetEmbeddedOidcFetchForTests () {
cachedFetch = undefined
}

function createEmbeddedOidcFetch () {
const material = getListenerTlsMaterial()
if (!material.enabled) {
return undefined
}

const caBuffers = getListenerTrustCaBuffers(material)
if (!caBuffers?.length) {
return undefined
}

if (!cachedFetch) {
const agent = new Agent({
connect: {
ca: caBuffers,
rejectUnauthorized: true
}
})
cachedFetch = (url, init) => undiciFetch(url, { ...init, dispatcher: agent })
}

return cachedFetch
}

module.exports = {
createEmbeddedOidcFetch,
resetEmbeddedOidcFetchForTests
}
34 changes: 27 additions & 7 deletions src/config/oidc.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,15 +282,33 @@ async function ensureEmbeddedOauthClient () {
const { issuerUrl } = getOidcSettings()
const db = require('../data/models')
const { resolveConfidentialClientSecret } = require('./embedded-oidc-client-secret')
const { getEmbeddedIssuerMetadata } = require('./embedded-oidc')
const { createEmbeddedOidcFetch } = require('./oidc-fetch')
const { clientId, clientSecret } = await resolveConfidentialClientSecret(db)

embeddedOauthClientPromise = oidcClient.discovery(
new URL(issuerUrl),
clientId,
clientSecret,
undefined,
getDiscoveryOptions()
).catch((error) => {
embeddedOauthClientPromise = (async () => {
const serverMetadata = getEmbeddedIssuerMetadata(issuerUrl)
const configuration = new oidcClient.Configuration(
serverMetadata,
clientId,
clientSecret,
undefined
)

const discoveryOptions = getDiscoveryOptions()
if (discoveryOptions?.execute) {
for (const extension of discoveryOptions.execute) {
extension(configuration)
}
}

const customFetch = createEmbeddedOidcFetch()
if (customFetch) {
configuration[oidcClient.customFetch] = customFetch
}

return configuration
})().catch((error) => {
embeddedOauthClientPromise = null
throw error
})
Expand All @@ -313,6 +331,8 @@ function resetDiscoveryForTests () {
issuerString = null
configuredClientId = null
oidcInstance = null
const { resetEmbeddedOidcFetchForTests } = require('./oidc-fetch')
resetEmbeddedOidcFetchForTests()
const { resetAuthSessionStoreForTests } = require('./auth-session-store')
resetAuthSessionStoreForTests()
}
Expand Down
28 changes: 10 additions & 18 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,18 +258,10 @@ initialize().then(() => {
const consoleURL = getConsoleUrl()
const consolePath = resolveConsolePath()

// File-based TLS configuration
const tlsKey = process.env.TLS_PATH_KEY || config.get('server.tls.path.key')
const tlsCert = process.env.TLS_PATH_CERT || config.get('server.tls.path.cert')
const intermedKey = process.env.TLS_PATH_INTERMEDIATE_CERT || config.get('server.tls.path.intermediateCert')

// Base64 TLS configuration
const tlsKeyBase64 = process.env.TLS_BASE64_KEY || config.get('server.tls.base64.key')
const tlsCertBase64 = process.env.TLS_BASE64_CERT || config.get('server.tls.base64.cert')
const intermedKeyBase64 = process.env.TLS_BASE64_INTERMEDIATE_CERT || config.get('server.tls.base64.intermediateCert')

const hasFileBasedTLS = !devMode && tlsKey && tlsCert
const hasBase64TLS = !devMode && tlsKeyBase64 && tlsCertBase64
const { getListenerTlsMaterial } = require('./utils/tls-config')
const tlsMaterial = getListenerTlsMaterial()
const hasFileBasedTLS = tlsMaterial.enabled && !tlsMaterial.isBase64
const hasBase64TLS = tlsMaterial.enabled && tlsMaterial.isBase64

consoleApp.use(express.static(consolePath, { index: 'index.html' }))
consoleApp.get('*', (req, res, next) => {
Expand Down Expand Up @@ -375,19 +367,19 @@ initialize().then(() => {
startHttpsServer(
{ api: app, console: consoleApp },
{ api: apiPort, console: consolePort },
tlsKey,
tlsCert,
intermedKey,
tlsMaterial.key,
tlsMaterial.cert,
tlsMaterial.intermediateCert,
jobs,
false
)
} else if (hasBase64TLS) {
startHttpsServer(
{ api: app, console: consoleApp },
{ api: apiPort, console: consolePort },
tlsKeyBase64,
tlsCertBase64,
intermedKeyBase64,
tlsMaterial.key,
tlsMaterial.cert,
tlsMaterial.intermediateCert,
jobs,
true
)
Expand Down
47 changes: 6 additions & 41 deletions src/services/agent-service.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
const config = require('../config')
const fs = require('fs')
// const Sequelize = require('sequelize')
const moment = require('moment')
// const Op = Sequelize.Op
const logger = require('../logger')
// const logger = require('../logger')

const TransactionDecorator = require('../decorators/transaction-decorator')
const FogProvisionKeyManager = require('../data/managers/iofog-provision-key-manager')
Expand Down Expand Up @@ -611,51 +610,17 @@ async function _checkMicroservicesFogType (fog, archId, transaction) {

const getControllerCA = async function (fog, transaction) {
const devMode = config.getBoolean('server.devMode', false)
const sslCert = process.env.SSL_CERT || config.get('server.ssl.path.cert')
const intermedKey = process.env.INTERMEDIATE_CERT || config.get('server.ssl.path.intermediateCert')
const sslCertBase64 = config.get('server.ssl.base64.cert')
const intermedKeyBase64 = config.get('server.ssl.base64.intermediateCert')
const hasFileBasedSSL = !devMode && sslCert
const hasBase64SSL = !devMode && sslCertBase64

if (devMode) {
throw new Errors.ValidationError('Controller is in development mode')
}

if (hasFileBasedSSL) {
try {
if (intermedKey) {
// Check if intermediate certificate file exists before trying to read it
if (fs.existsSync(intermedKey)) {
const certData = fs.readFileSync(intermedKey, 'utf8')
return Buffer.from(certData).toString('base64')
} else {
// Intermediate certificate file doesn't exist, don't provide any CA cert
// Let the system's default trust store handle validation
logger.info(`Intermediate certificate file not found at path: ${intermedKey}, not providing CA certificate`)
return ''
}
} else {
// No intermediate certificate path provided, don't provide any CA cert
// Let the system's default trust store handle validation
return ''
}
} catch (error) {
throw new Errors.ValidationError('Failed to read SSL certificate file')
}
}

if (hasBase64SSL) {
if (intermedKeyBase64) {
return intermedKeyBase64
} else {
// No intermediate certificate base64 provided, don't provide any CA cert
// Let the system's default trust store handle validation
return ''
}
const { getListenerTlsMaterial, getListenerTrustCaBase64 } = require('../utils/tls-config')
const material = getListenerTlsMaterial()
if (!material.enabled) {
throw new Errors.ValidationError('No valid SSL certificate configuration found')
}

throw new Errors.ValidationError('No valid SSL certificate configuration found')
return getListenerTrustCaBase64(material)
}

// New endpoint: Get active log sessions for agent
Expand Down
45 changes: 2 additions & 43 deletions src/services/iofog-service.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const config = require('../config')
const fs = require('fs')
const TransactionDecorator = require('../decorators/transaction-decorator')
const AppHelper = require('../helpers/app-helper')
const FogManager = require('../data/managers/iofog-manager')
Expand Down Expand Up @@ -1094,49 +1093,9 @@ async function generateProvisioningKeyEndPoint (fogData, isCLI, transaction) {
}

const provisioningKeyData = await refreshProvisionKeyForFog(fogData.uuid, transaction)
const { getListenerTrustCaBase64 } = require('../utils/tls-config')
const caCert = getListenerTrustCaBase64()

const devMode = config.getBoolean('server.devMode', false)
const sslCert = process.env.SSL_CERT || config.get('server.ssl.path.cert')
const intermedKey = process.env.INTERMEDIATE_CERT || config.get('server.ssl.path.intermediateCert')
const sslCertBase64 = config.get('server.ssl.base64.cert')
const intermedKeyBase64 = config.get('server.ssl.base64.intermediateCert')
const hasFileBasedSSL = !devMode && sslCert
const hasBase64SSL = !devMode && sslCertBase64
let caCert = ''

if (!devMode) {
if (hasFileBasedSSL) {
try {
if (intermedKey) {
// Check if intermediate certificate file exists before trying to read it
if (fs.existsSync(intermedKey)) {
const certData = fs.readFileSync(intermedKey)
caCert = Buffer.from(certData).toString('base64')
} else {
// Intermediate certificate file doesn't exist, don't provide any CA cert
// Let the system's default trust store handle validation
logger.info(`Intermediate certificate file not found at path: ${intermedKey}, not providing CA certificate`)
caCert = ''
}
} else {
// No intermediate certificate path provided, don't provide any CA cert
// Let the system's default trust store handle validation
caCert = ''
}
} catch (error) {
throw new Errors.ValidationError('Failed to read SSL certificate file')
}
}
if (hasBase64SSL) {
if (intermedKeyBase64) {
caCert = intermedKeyBase64
} else {
// No intermediate certificate base64 provided, don't provide any CA cert
// Let the system's default trust store handle validation
caCert = ''
}
}
}
return {
key: provisioningKeyData.provisionKey,
expirationTime: provisioningKeyData.expirationTime,
Expand Down
Loading