Skip to content

Commit 7d91709

Browse files
committed
feat: add configurable FIPS 140-3 mode across all microservices
Make FIPS enforcement opt-in via a `fips_mode` config property delivered through user-provided services, consistent with how all other config is transported. Default is off so non-FIPS deployments are unaffected. Go services: add FipsMode to config structs, make AssertFIPSMode() conditional, log activation status, expose autoscaler_fips_enabled Prometheus gauge (0/1) on health endpoints. Java scheduler: add isFipsModeEnabled() reading VCAP_SERVICES, make FipsSecurityProviderConfig.initialize() conditional, register autoscaler_fips_enabled gauge in HealthExporter. Deployment: remove GOFIPS140 from default mta.yaml, add build-extension-file-fips Make target generating a FIPS MTA extension that sets GOFIPS140/GODEBUG on Go modules and fips_mode in service configs. mta-deploy auto-applies the FIPS extension when present. MTA: add provides sections to service modules for URL sharing, add acceptance-tests-config user-provided service resource, refactor acceptance config to load from VCAP_SERVICES. CI: add acceptance_tests_fips_mta.yaml workflow, extend reusable workflow with build_fips_extension input. Add fips acceptance test suite that validates autoscaler_fips_enabled metric on all services.
1 parent d93aaca commit 7d91709

36 files changed

Lines changed: 618 additions & 63 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Acceptance Tests - FIPS
2+
on:
3+
workflow_dispatch:
4+
pull_request:
5+
types: [ opened, synchronize ]
6+
paths:
7+
- '**/*fips*'
8+
- '**/*Fips*'
9+
- 'startup/startup.go'
10+
- 'scheduler/src/main/java/**/SchedulerApplication.java'
11+
- '.github/workflows/acceptance_tests_fips.yaml'
12+
13+
concurrency:
14+
group: "${{ github.workflow }}/${{ github.ref }}"
15+
cancel-in-progress: true
16+
17+
jobs:
18+
acceptance_tests_fips:
19+
name: "${{ github.workflow }}"
20+
uses: ./.github/workflows/acceptance_tests_reusable.yaml
21+
with:
22+
deployment_name: "autoscaler-fips-${{ github.event.pull_request.number || github.run_number }}"
23+
build_fips_extension: true
24+
suites: "[ 'api', 'app', 'broker', 'fips' ]"
25+
secrets:
26+
bbl_ssh_key: "${{ secrets.BBL_SSH_KEY }}"

.github/workflows/acceptance_tests_reusable.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ on:
99
deployment_name:
1010
required: true
1111
type: string
12+
build_fips_extension:
13+
required: false
14+
type: boolean
15+
default: false
1216
secrets:
1317
bbl_ssh_key:
1418
required: true
@@ -58,6 +62,9 @@ jobs:
5862
shell: bash
5963
run: |
6064
make --directory="${AUTOSCALER_DIR}" build-extension-file
65+
if [ "${{ inputs.build_fips_extension }}" = "true" ]; then
66+
make --directory="${AUTOSCALER_DIR}" build-extension-file-fips
67+
fi
6168
make --directory="${AUTOSCALER_DIR}" mta-build
6269
make --directory="${AUTOSCALER_DIR}" mta-deploy
6370

Makefile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,9 @@ mta-undeploy:
299299
build-extension-file:
300300
$(MAKEFILE_DIR)/scripts/build-extension-file.sh
301301

302+
build-extension-file-fips:
303+
$(MAKEFILE_DIR)/scripts/build-extension-file-fips.sh
304+
302305
mta-logs:
303306
rm -rf mta-*
304307
cf dmol --mta com.github.cloudfoundry.app-autoscaler-release --last 1
@@ -507,7 +510,7 @@ scheduler.test: check-db_type scheduler.test-certificates init-db
507510
scheduler.test-certificates:
508511
make --directory=scheduler test-certificates
509512

510-
lint: lint-go lint-actions lint-markdown
513+
lint: lint-go lint-actions lint-markdown lint-shell
511514
.PHONY: lint-go
512515
lint-go: generate-openapi-generated-clients-and-servers generate-fakes acceptance.lint test-app.lint gorouterproxy.lint
513516
readonly GOVERSION='${GO_VERSION}' ;\
@@ -520,6 +523,11 @@ lint-actions:
520523
@echo " - linting GitHub actions"
521524
actionlint
522525

526+
.PHONY: lint-shell
527+
lint-shell:
528+
@echo " - linting shell scripts"
529+
shellcheck scripts/*.sh
530+
523531
acceptance.lint:
524532
@echo 'Linting acceptance-tests …'
525533
make --directory='acceptance' lint

acceptance/config/config.go

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"os"
66
"path/filepath"
7+
"slices"
78
"strings"
89
"time"
910

@@ -82,10 +83,14 @@ type Config struct {
8283

8384
HealthEndpointsBasicAuthEnabled bool `json:"health_endpoints_basic_auth_enabled"`
8485

86+
HealthEndpoints map[string]HealthEndpointConfig `json:"health_endpoints"`
87+
8588
CPUUpperThreshold int64 `json:"cpu_upper_threshold"`
8689

8790
CPUUtilScalingPolicyTest CPUUtilScalingPolicyTest `json:"cpuutil_scaling_policy_test"`
8891

92+
FipsModeExpected bool `json:"fips_mode_expected"`
93+
8994
Performance PerformanceConfig `json:"performance"`
9095
}
9196

@@ -94,6 +99,12 @@ type CPUUtilScalingPolicyTest struct {
9499
AppCPUEntitlement int `json:"app_cpu_entitlement"`
95100
}
96101

102+
type HealthEndpointConfig struct {
103+
Endpoint string `json:"endpoint"`
104+
Username string `json:"username"`
105+
Password string `json:"password"`
106+
}
107+
97108
var defaults = Config{
98109
AddExistingUserToExistingSpace: true,
99110

@@ -152,20 +163,18 @@ type TerminateSuite func(message string, callerSkip ...int)
152163
var DefaultTerminateSuite TerminateSuite = ginkgo.AbortSuite
153164

154165
func LoadConfig(terminateSuite TerminateSuite) *Config {
155-
// First check for direct JSON config in environment variable
156-
configJSON := os.Getenv("ACCEPTANCE_CONFIG_JSON")
157-
158166
config := defaults
159167
var err error
160168

161-
if configJSON != "" {
162-
// Parse JSON directly from env var
169+
// Priority: VCAP_SERVICES (CF deployment) > ACCEPTANCE_CONFIG_JSON (env) > CONFIG file
170+
if configJSON := loadConfigFromVCAPServices(); configJSON != "" {
171+
err = loadConfigFromJSON(configJSON, &config)
172+
} else if configJSON := os.Getenv("ACCEPTANCE_CONFIG_JSON"); configJSON != "" {
163173
err = loadConfigFromJSON(configJSON, &config)
164174
} else {
165-
// Fall back to file-based config (existing behavior)
166175
path := os.Getenv("CONFIG")
167176
if path == "" {
168-
terminateSuite("Must set $CONFIG to point to a json file or $ACCEPTANCE_CONFIG_JSON with JSON content")
177+
terminateSuite("Must set $CONFIG to point to a json file, $ACCEPTANCE_CONFIG_JSON with JSON content, or bind an acceptance-tests-config service")
169178
}
170179
err = loadConfigFromPath(path, &config)
171180
}
@@ -280,6 +289,61 @@ func loadConfigFromJSON(jsonContent string, config *Config) error {
280289
return decoder.Decode(config)
281290
}
282291

292+
// loadConfigFromVCAPServices extracts acceptance test config from the
293+
// acceptance-tests-config user-provided service in VCAP_SERVICES.
294+
// Returns the credentials JSON string, or "" if not found.
295+
func loadConfigFromVCAPServices() string {
296+
vcapServices := os.Getenv("VCAP_SERVICES")
297+
if vcapServices == "" {
298+
return ""
299+
}
300+
301+
var services map[string][]struct {
302+
Tags []string `json:"tags"`
303+
Credentials map[string]interface{} `json:"credentials"`
304+
}
305+
if err := json.Unmarshal([]byte(vcapServices), &services); err != nil {
306+
return ""
307+
}
308+
309+
svc, found := findServiceByTag(services, "acceptance-tests-config")
310+
if !found {
311+
return ""
312+
}
313+
314+
creds, ok := svc.Credentials["acceptance-tests-config"]
315+
if !ok {
316+
return ""
317+
}
318+
319+
credBytes, err := json.Marshal(creds)
320+
if err != nil {
321+
return ""
322+
}
323+
return string(credBytes)
324+
}
325+
326+
func findServiceByTag(services map[string][]struct {
327+
Tags []string `json:"tags"`
328+
Credentials map[string]interface{} `json:"credentials"`
329+
}, tag string) (struct {
330+
Tags []string `json:"tags"`
331+
Credentials map[string]interface{} `json:"credentials"`
332+
}, bool) {
333+
for _, instances := range services {
334+
for _, svc := range instances {
335+
if slices.Contains(svc.Tags, tag) {
336+
return svc, true
337+
}
338+
}
339+
}
340+
var zero struct {
341+
Tags []string `json:"tags"`
342+
Credentials map[string]interface{} `json:"credentials"`
343+
}
344+
return zero, false
345+
}
346+
283347
func (c *Config) Protocol() string {
284348
if c.UseHttp {
285349
return "http://"

acceptance/config/config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func TestConfigSuite(t *testing.T) {
1919
var _ = Describe("LoadConfig", func() {
2020
When("CONFIG env var not set", func() {
2121
It("terminates suite", func() {
22-
loadConfigExpectSuiteTerminationWith("Must set $CONFIG to point to a json file or $ACCEPTANCE_CONFIG_JSON with JSON content")
22+
loadConfigExpectSuiteTerminationWith("Must set $CONFIG to point to a json file, $ACCEPTANCE_CONFIG_JSON with JSON content, or bind an acceptance-tests-config service")
2323
})
2424
})
2525

acceptance/fips/fips_mode_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package fips_test
2+
3+
import (
4+
"bufio"
5+
"crypto/tls"
6+
"fmt"
7+
"io"
8+
"net"
9+
"net/http"
10+
"strconv"
11+
"strings"
12+
"time"
13+
14+
. "github.com/onsi/ginkgo/v2"
15+
. "github.com/onsi/gomega"
16+
)
17+
18+
var _ = Describe("FIPS Mode Verification", func() {
19+
var client *http.Client
20+
21+
BeforeEach(func() {
22+
client = &http.Client{
23+
Transport: &http.Transport{
24+
Proxy: http.ProxyFromEnvironment,
25+
DialContext: (&net.Dialer{
26+
Timeout: 30 * time.Second,
27+
KeepAlive: 30 * time.Second,
28+
}).DialContext,
29+
TLSHandshakeTimeout: 10 * time.Second,
30+
DisableCompression: true,
31+
DisableKeepAlives: true,
32+
TLSClientConfig: &tls.Config{
33+
InsecureSkipVerify: cfg.SkipSSLValidation,
34+
},
35+
},
36+
Timeout: 30 * time.Second,
37+
}
38+
})
39+
40+
It("verifies FIPS mode status on all configured health endpoints", func() {
41+
Expect(cfg.HealthEndpoints).NotTo(BeEmpty(),
42+
"No health_endpoints configured — cannot verify FIPS mode")
43+
44+
expectedValue := float64(0)
45+
if cfg.FipsModeExpected {
46+
expectedValue = 1
47+
}
48+
49+
for name, ep := range cfg.HealthEndpoints {
50+
url := ep.Endpoint
51+
if !strings.HasPrefix(url, "http") {
52+
url = "https://" + url
53+
}
54+
55+
By(fmt.Sprintf("Checking FIPS status on %s at %s", name, url))
56+
57+
req, err := http.NewRequest("GET", url, nil)
58+
Expect(err).NotTo(HaveOccurred())
59+
60+
if ep.Username != "" {
61+
req.SetBasicAuth(ep.Username, ep.Password)
62+
}
63+
64+
resp, err := client.Do(req)
65+
Expect(err).NotTo(HaveOccurred(), "Failed to reach %s health endpoint", name)
66+
defer resp.Body.Close()
67+
68+
Expect(resp.StatusCode).To(Equal(http.StatusOK),
69+
"Health endpoint for %s returned status %d", name, resp.StatusCode)
70+
71+
fipsValue, found := parseFipsMetric(resp.Body)
72+
Expect(found).To(BeTrue(),
73+
"autoscaler_fips_enabled metric not found on %s", name)
74+
Expect(fipsValue).To(Equal(expectedValue),
75+
"FIPS mode mismatch on %s: expected %v, got %v", name, expectedValue, fipsValue)
76+
}
77+
})
78+
})
79+
80+
// parseFipsMetric scans Prometheus text output for the autoscaler_fips_enabled metric.
81+
func parseFipsMetric(body io.Reader) (float64, bool) {
82+
scanner := bufio.NewScanner(body)
83+
for scanner.Scan() {
84+
line := scanner.Text()
85+
if strings.HasPrefix(line, "#") {
86+
continue
87+
}
88+
if strings.HasPrefix(line, "autoscaler_fips_enabled") {
89+
parts := strings.Fields(line)
90+
if len(parts) >= 2 {
91+
val, err := strconv.ParseFloat(parts[1], 64)
92+
if err == nil {
93+
return val, true
94+
}
95+
}
96+
}
97+
}
98+
return 0, false
99+
}

acceptance/fips/fips_suite_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package fips_test
2+
3+
import (
4+
"testing"
5+
6+
"acceptance/config"
7+
8+
. "github.com/onsi/ginkgo/v2"
9+
. "github.com/onsi/gomega"
10+
)
11+
12+
var (
13+
cfg *config.Config
14+
)
15+
16+
const componentName = "FIPS Mode Suite"
17+
18+
func TestAcceptance(t *testing.T) {
19+
RegisterFailHandler(Fail)
20+
RunSpecs(t, componentName)
21+
}
22+
23+
var _ = BeforeSuite(func() {
24+
cfg = config.LoadConfig(config.DefaultTerminateSuite)
25+
})

api/cmd/api/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func main() {
2828
os.Exit(1)
2929
}
3030

31-
startup.SetupEnvironment()
31+
startup.SetupEnvironment(conf.FipsMode)
3232

3333
logger := startup.InitLogger(&conf.Logging, "api")
3434

api/config/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ type Config struct {
121121
StoredProcedureConfig *models.StoredProcedureConfig `yaml:"stored_procedure_binding_credential_config" json:"stored_procedure_binding_credential_config"`
122122
ScalingRules ScalingRulesConfig `yaml:"scaling_rules" json:"scaling_rules"`
123123
DefaultCustomMetricsCredentialType string `yaml:"default_credential_type" json:"default_credential_type"`
124+
FipsMode bool `yaml:"fips_mode" json:"fips_mode"`
124125
}
125126

126127
func (c *Config) SetLoggingLevel() {
@@ -132,6 +133,11 @@ func (c *Config) GetLogging() *helpers.LoggingConfig {
132133
return &c.Logging
133134
}
134135

136+
// GetFipsMode returns whether FIPS 140-3 mode is configured
137+
func (c *Config) GetFipsMode() bool {
138+
return c.FipsMode
139+
}
140+
135141
type PlanCheckConfig struct {
136142
PlanDefinitions map[string]PlanDefinition `yaml:"plan_definitions" json:"plan_definitions"`
137143
}

api/publicapiserver/public_api_server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ func (s *PublicApiServer) createPrometheusRegistry() *prometheus.Registry {
180180
healthendpoint.NewDatabaseStatusCollector("autoscaler", "golangapiserver", "policyDB", s.policyDB),
181181
healthendpoint.NewDatabaseStatusCollector("autoscaler", "golangapiserver", "bindingDB", s.bindingDB),
182182
s.httpStatusCollector,
183+
helpers.FipsEnabledGauge,
183184
},
184185
true, s.logger.Session("golangapiserver-prometheus"))
185186
return promRegistry

0 commit comments

Comments
 (0)