Repository: Azure/ARO-RP
Pull request #1451 (2021-04-19 20:25:56)

Cloudfit/guardianwork
### Which issue this PR addresses:

Fixes https://msazure.visualstudio.com/AzureRedHatOpenShift/_workitems/edit/9756109

### What this PR does / why we need it:

This PR accomplishes the following items:
* Moved Onebranch pipelines (yml) from internal ADO instance to GitHub
* Includes a .gdn directory created by Guardian.  This directory includes the .gdnsuppress, which allowed us to suppress the ARM1005 finding until a fix is complete

### Test plan for issue:

https://msazure.visualstudio.com/AzureRedHatOpenShift/_build/results?buildId=41592363&view=results
https://msazure.visualstudio.com/AzureRedHatOpenShift/_build/results?buildId=41593870&view=results

Both builds now successfully run and are able to publish artifacts that can be consumed by Ev2

### Is there any documentation that needs to be updated for this PR?

N/A


jewzaam: /azp run E2E
azure-pipelines[bot]: <samp>
Azure Pipelines successfully started running 1 pipeline(s).<br>

</samp>
jewzaam: /azp run CI
azure-pipelines[bot]: <samp>
Azure Pipelines successfully started running 1 pipeline(s).<br>

</samp>
mikeandescavage: /azp run CI
azure-pipelines[bot]: <samp>
Commenter does not have sufficient privileges for PR 1451 in repo Azure/ARO-RP<br>

</samp>

Patch:
diff --git a/.config/CredScanSuppressions.json b/.config/CredScanSuppressions.json
new file mode 100644
index 000000000..0bcc58d1c
--- /dev/null
+++ b/.config/CredScanSuppressions.json
@@ -0,0 +1,33 @@
+{
+    "tool": "Credential Scanner",
+    "suppressions": [
+        {
+            "file": "proxy_test.go",
+            "_justification": "sample login for testing"
+        },
+        {
+            "file": "pullsecret_test.go",
+            "_justification": "sample login for testing"
+        },
+        {
+            "file": "viper.go",
+            "_justification": "example login from comments"
+        },
+        {
+            "file": "README.md",
+            "_justification": "example login from comments"
+        },
+        {
+            "file": "api.go",
+            "_justification": "example secret in comment"
+        },
+        {
+            "file": "machine_webhook.go",
+            "_justification": "false positive"
+        },
+        {
+            "file": "doc.go",
+            "_justification": "examples in comments"
+        }
+    ]
+}
diff --git a/.gdn/.gdnsettings b/.gdn/.gdnsettings
new file mode 100644
index 000000000..156408362
--- /dev/null
+++ b/.gdn/.gdnsettings
@@ -0,0 +1,7 @@
+{
+  "files": { },
+  "folders": { },
+  "overwriteLogs": true,
+  "telemetryFlushTimeout": 10,
+  "variables": { }
+}
\ No newline at end of file
diff --git a/.gdn/.gdnsuppress b/.gdn/.gdnsuppress
new file mode 100644
index 000000000..af4018ac4
--- /dev/null
+++ b/.gdn/.gdnsuppress
@@ -0,0 +1,415 @@
+{
+  "version": "1.0.0",
+  "suppressionSets": {
+    "default": {
+      "name": "default",
+      "createdDate": "2021-04-19 17:58:41Z",
+      "lastUpdatedDate": "2021-04-19 17:58:41Z"
+    }
+  },
+  "results": {
+    "f02a038e5366a1cd9fa0387dab25be4965a56ada4fafb0c467bb3c9b7e7e442f": {
+      "signature": "f02a038e5366a1cd9fa0387dab25be4965a56ada4fafb0c467bb3c9b7e7e442f",
+      "target": "deploy/cluster-predeploy.json",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "ARMory",
+      "ruleId": "ARM1005",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "986ee7027f0344a26e6b23ffc6882d572558701e70517ac8c8ea879c2a29a868": {
+      "signature": "986ee7027f0344a26e6b23ffc6882d572558701e70517ac8c8ea879c2a29a868",
+      "target": "pkg/portal/assets/index.js",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "ESLint",
+      "ruleId": "@microsoft/sdl/no-html-method",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "b857fede421e892627bd6da61ce8af1268f05a08cea9e85144c18dd5173d3e35": {
+      "signature": "b857fede421e892627bd6da61ce8af1268f05a08cea9e85144c18dd5173d3e35",
+      "target": "pkg/portal/assets/index.js",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "ESLint",
+      "ruleId": "@microsoft/sdl/no-html-method",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "017e16e16457f4a678f671f22265319a30b21b8a1cc0c92afdf53f60ec2b0cdd": {
+      "signature": "017e16e16457f4a678f671f22265319a30b21b8a1cc0c92afdf53f60ec2b0cdd",
+      "target": "vendor/go.etcd.io/bbolt/README.md",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "183125",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "9d0e60cccd9461c6197f73baf0990a8f1b33e3493ce1f92ece8c707da7b89a3c": {
+      "signature": "9d0e60cccd9461c6197f73baf0990a8f1b33e3493ce1f92ece8c707da7b89a3c",
+      "target": "vendor/google.golang.org/grpc/README.md",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "79569",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "630dcedab4a607f2cdb6919e4eaa8af987f33a3e1bd0bab80efdf27290b31f9f": {
+      "signature": "630dcedab4a607f2cdb6919e4eaa8af987f33a3e1bd0bab80efdf27290b31f9f",
+      "target": "pkg/portal/assets/lib/bootstrap-4.5.2.min.js",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "ESLint",
+      "ruleId": "@microsoft/sdl/no-html-method",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "9e93335f4998702254fec6081c9782ba12cdb90d818a25742717c4895e055cd0": {
+      "signature": "9e93335f4998702254fec6081c9782ba12cdb90d818a25742717c4895e055cd0",
+      "target": "pkg/portal/assets/lib/bootstrap-4.5.2.min.js",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "80411",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "fb3dac99dd840709a8197044f336e613515155803bc89dc65a5e42d2f6c65d9e": {
+      "signature": "fb3dac99dd840709a8197044f336e613515155803bc89dc65a5e42d2f6c65d9e",
+      "target": "pkg/portal/assets/lib/bootstrap-select-1.13.14.min.js",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "ESLint",
+      "ruleId": "@microsoft/sdl/no-inner-html",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "a87cae7679649d21e179514bbda8cecc135a9bc6d2450c4a431f12e6564b4a26": {
+      "signature": "a87cae7679649d21e179514bbda8cecc135a9bc6d2450c4a431f12e6564b4a26",
+      "target": "pkg/portal/assets/lib/bootstrap-select-1.13.14.min.js",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "80411",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "e0ea2773e69dfa94138d7e59a66ef5284f2210e4062ed5d88b658db8fb515fc9": {
+      "signature": "e0ea2773e69dfa94138d7e59a66ef5284f2210e4062ed5d88b658db8fb515fc9",
+      "target": "pkg/portal/assets/lib/jquery-3.5.1.min.js",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "ESLint",
+      "ruleId": "@microsoft/sdl/no-inner-html",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "53a6fd40496c6da617ef3de2ee99d40fe7ec9cb74056e96ee6e278c7ac76d8c9": {
+      "signature": "53a6fd40496c6da617ef3de2ee99d40fe7ec9cb74056e96ee6e278c7ac76d8c9",
+      "target": "pkg/portal/assets/lib/jquery-3.5.1.min.js",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "ESLint",
+      "ruleId": "@microsoft/sdl/no-html-method",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "4f6f0c9562220288a4d2ef92d458ee4a3d02375491e5281a04a4e1245eb5cd67": {
+      "signature": "4f6f0c9562220288a4d2ef92d458ee4a3d02375491e5281a04a4e1245eb5cd67",
+      "target": "pkg/portal/assets/lib/popper-1.12.9.min.js",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "211972",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "0a658656743ed3c5cb8f34b7da4861fdcc579472ba4013f0b20e2ffb0f6278db": {
+      "signature": "0a658656743ed3c5cb8f34b7da4861fdcc579472ba4013f0b20e2ffb0f6278db",
+      "target": "vendor/github.com/Djarvur/go-err113/.golangci.yml",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "79459",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "71872f574e2dd41672f3de8d647d1467f973e6786d04df6b01df8c536134b190": {
+      "signature": "71872f574e2dd41672f3de8d647d1467f973e6786d04df6b01df8c536134b190",
+      "target": "vendor/github.com/golangci/misspell/README.md",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "79570",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "ac45307b7f52c6a06cd1e2e15f2d88367c6d7ea42beb3bd6e1741f2f1b56d342": {
+      "signature": "ac45307b7f52c6a06cd1e2e15f2d88367c6d7ea42beb3bd6e1741f2f1b56d342",
+      "target": "vendor/github.com/gorilla/csrf/README.md",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "80411",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "d8cdc23f09bfb5154317f9d7f84ee0058c7f3607372cf2f93307f6a2aa2b2397": {
+      "signature": "d8cdc23f09bfb5154317f9d7f84ee0058c7f3607372cf2f93307f6a2aa2b2397",
+      "target": "vendor/github.com/matoous/godox/.golangci.yml",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "79459",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "c3ad2e7173a56c742c1f55d549ea9aa1169806f46b649fb36fa5c128a4dd5d13": {
+      "signature": "c3ad2e7173a56c742c1f55d549ea9aa1169806f46b649fb36fa5c128a4dd5d13",
+      "target": "vendor/github.com/onsi/ginkgo/CHANGELOG.md",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "183125",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "f572c9bc592da3e9285b510fe8929d4bfe0c51ef69ea1e298261870c62d431d0": {
+      "signature": "f572c9bc592da3e9285b510fe8929d4bfe0c51ef69ea1e298261870c62d431d0",
+      "target": "vendor/github.com/OpenPeeDeeP/depguard/README.md",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "80409",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "abc528ba2ee6fad85ea075e9eb745a016dcd08c4319cbe3dd02e55bf4760de96": {
+      "signature": "abc528ba2ee6fad85ea075e9eb745a016dcd08c4319cbe3dd02e55bf4760de96",
+      "target": "vendor/github.com/OpenPeeDeeP/depguard/README.md",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "79458",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "239ea87a93e65e1a44704dda0927367c5631790451b2d8791520d886dcbefe06": {
+      "signature": "239ea87a93e65e1a44704dda0927367c5631790451b2d8791520d886dcbefe06",
+      "target": "vendor/github.com/OpenPeeDeeP/depguard/README.md",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "80411",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "c9c4231a7c7ec4d3e99bd137c4ad56d79b61286280324395d34deb7a93855ac5": {
+      "signature": "c9c4231a7c7ec4d3e99bd137c4ad56d79b61286280324395d34deb7a93855ac5",
+      "target": "vendor/github.com/OpenPeeDeeP/depguard/README.md",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "80411",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "fc721b1a51b4e6cc1f0c421114d64415e72d0343f26e4eadb9e3cc8aea3f8dd7": {
+      "signature": "fc721b1a51b4e6cc1f0c421114d64415e72d0343f26e4eadb9e3cc8aea3f8dd7",
+      "target": "vendor/github.com/OpenPeeDeeP/depguard/README.md",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "79459",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "435bc256cdfdafdf1998f4ac193889c085d6c33248098e1ececa084f5a848879": {
+      "signature": "435bc256cdfdafdf1998f4ac193889c085d6c33248098e1ececa084f5a848879",
+      "target": "vendor/github.com/OpenPeeDeeP/depguard/README.md",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "79459",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "b10fdc18ed5ab1ad827e2818710485811999407cc75325511412d7313a7e04b9": {
+      "signature": "b10fdc18ed5ab1ad827e2818710485811999407cc75325511412d7313a7e04b9",
+      "target": "vendor/gopkg.in/AlecAivazis/survey.v1/README.md",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "80241",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "ad62bb8858c3213271808f0ca500a7db9c8d4ef255505a6e49abd719858dd3e0": {
+      "signature": "ad62bb8858c3213271808f0ca500a7db9c8d4ef255505a6e49abd719858dd3e0",
+      "target": "vendor/google.golang.org/api/compute/v1/compute-api.json",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "80411",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "8f798268a0ff7469696f2c43b0d3e4ff70bdd2a7f676430651b44628a76648b9": {
+      "signature": "8f798268a0ff7469696f2c43b0d3e4ff70bdd2a7f676430651b44628a76648b9",
+      "target": "vendor/google.golang.org/api/compute/v1/compute-api.json",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "209526",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "47ea1c58d8afd46f92d9ba1f84983dcfc6aacb2f1c7506c08ddcfa9f69cbbe61": {
+      "signature": "47ea1c58d8afd46f92d9ba1f84983dcfc6aacb2f1c7506c08ddcfa9f69cbbe61",
+      "target": "vendor/google.golang.org/api/compute/v1/compute-api.json",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "79459",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "5a6392b55b81eccf9ca74a09d4c76f6beeb1975b8e232e662f2e1ac7a264fd17": {
+      "signature": "5a6392b55b81eccf9ca74a09d4c76f6beeb1975b8e232e662f2e1ac7a264fd17",
+      "target": "vendor/github.com/openshift/api/security/v1/0000_03_security-openshift_01_scc.crd.yaml",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "80409",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "0a91b109e86130d46f7038d7d81266228e886e6d70d1ce0f4e3217f6f3306e36": {
+      "signature": "0a91b109e86130d46f7038d7d81266228e886e6d70d1ce0f4e3217f6f3306e36",
+      "target": "vendor/github.com/openshift/api/security/v1/0000_03_security-openshift_01_scc.crd.yaml",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "80411",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "2ad4a9019f0448d937ead179db770d5cc42b9a75cd07c93fe091af259d891b9a": {
+      "signature": "2ad4a9019f0448d937ead179db770d5cc42b9a75cd07c93fe091af259d891b9a",
+      "target": "vendor/github.com/openshift/api/security/v1/0000_03_security-openshift_01_scc.crd.yaml",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "80411",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    },
+    "6348b36cd4ecfa496869b5fd0fc914e3d254907186edecf953935a63530f78e0": {
+      "signature": "6348b36cd4ecfa496869b5fd0fc914e3d254907186edecf953935a63530f78e0",
+      "target": "vendor/github.com/openshift/api/security/v1/0000_03_security-openshift_01_scc.crd.yaml",
+      "memberOf": [
+        "default"
+      ],
+      "tool": "policheck",
+      "ruleId": "80409",
+      "justification": null,
+      "createdDate": "2021-04-19 17:58:41Z",
+      "expirationDate": null,
+      "type": null
+    }
+  }
+}
\ No newline at end of file
diff --git a/.gdn/.gitignore b/.gdn/.gitignore
new file mode 100644
index 000000000..a147f7d08
--- /dev/null
+++ b/.gdn/.gitignore
@@ -0,0 +1,10 @@
+﻿## Ignore Guardian internal files
+.r/
+rc/
+rs/
+i/
+p/
+c/
+
+## Ignore Guardian Local settings
+LocalSettings.gdn.json
diff --git a/.pipelines/onebranch/pipeline.buildrp.official.yml b/.pipelines/onebranch/pipeline.buildrp.official.yml
new file mode 100644
index 000000000..26aebade2
--- /dev/null
+++ b/.pipelines/onebranch/pipeline.buildrp.official.yml
@@ -0,0 +1,125 @@
+#################################################################################
+#                               OneBranch Pipelines                             #
+# This pipeline was created by EasyStart from a sample located at:              #
+#   https://aka.ms/obpipelines/easystart/samples                                #
+# Documentation:  https://aka.ms/obpipelines                                    #
+# Yaml Schema:    https://aka.ms/obpipelines/yaml/schema                        #
+# Retail Tasks:   https://aka.ms/obpipelines/tasks                              #
+# Support:        https://aka.ms/onebranchsup                                   #
+#################################################################################
+
+trigger: none
+pr: none
+
+variables:
+  CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] # needed for onebranch.pipeline.version task https://aka.ms/obpipelines/versioning
+  LinuxContainerImage: cdpxlinux.azurecr.io/global/ubuntu-1804-all:5.0   # Docker image which is used to build the project https://aka.ms/obpipelines/containers
+  DEBIAN_FRONTEND: noninteractive
+
+resources:
+  repositories:
+  - repository: templates
+    type: git
+    name: OneBranch.Pipelines/GovernedTemplates
+    ref: refs/heads/main
+  - repository: rhado
+    type: git
+    name: ARO.Pipelines
+
+extends:
+  template: v2/OneBranch.Official.CrossPlat.yml@templates # https://aka.ms/obpipelines/templates
+  parameters:
+    globalSdl: # https://aka.ms/obpipelines/sdl
+      # tsa:
+      #  enabled: true # SDL results of non-official builds aren't uploaded to TSA by default.
+      # credscan:
+      #   suppressionsFile: $(Build.SourcesDirectory)\.config\CredScanSuppressions.json
+      policheck:
+        break: true # always break the build on policheck issues. You can disable it by setting to 'false'
+      suppression:
+        suppressionFile: $(Build.SourcesDirectory)\.gdn\.gdnsuppress
+        suppressionSet: default
+
+    stages:
+    - stage: Build_Ev2_Artifacts
+      jobs:
+      - job: Build_Ev2_Artifacts
+        pool:
+          type: linux
+
+        variables: # More settings at https://aka.ms/obpipelines/yaml/jobs
+          ob_outputDirectory: $(Build.SourcesDirectory)/out   # this directory is uploaded to pipeline artifacts, reddog and cloudvault. More info at https://aka.ms/obpipelines/artifacts
+
+        steps:
+        - checkout: rhado
+        - task: GoTool@0
+          displayName: Use Go 1.16.2
+          inputs:
+            version: 1.16.2
+        - task: Go@0
+          inputs:
+            command: custom
+            customCommand: run
+            arguments: . --config-path ./config/
+            workingDirectory: $(Build.SourcesDirectory)/ARO.Pipelines/ev2/generator/
+          displayName: ⚙️ Generate Ev2 Deployment Manifests
+        - task: Bash@3
+          displayName: ⚙️ Copy to ob_outputDirectory
+          inputs:
+            targetType: filePath
+            filePath: .pipelines/onebranch/scripts/deploymentpipeline.sh
+
+# https://onebranch.visualstudio.com/OneBranch/_wiki/wikis/OneBranch.wiki/4605/Artifacts
+
+    - stage: Build_Docker_Image
+      jobs:
+      - job: Build_Docker_Image
+        pool:
+          type: docker
+          os: linux
+        variables:
+          ob_git_checkout: true
+        steps:
+        - task: onebranch.pipeline.imagebuildinfo@1
+          displayName: Build Multi Stage Dockerfile
+          inputs:
+            repositoryName: aro-rp
+            dockerFileRelPath: ./Dockerfile.aro-multistage
+            dockerFileContextPath: ./
+            registry: cdpxlinux.azurecr.io
+            saveImageToPath: aro-rp.tar
+            buildkit: 1
+            enable_network: true
+            build_tag: $(Build.BuildNumber)
+
+
+    - stage: Build_ARO
+      jobs:
+      - job: Build_ARO
+        pool:
+          type: linux
+
+        variables: # More settings at https://aka.ms/obpipelines/yaml/jobs
+          ob_outputDirectory: $(Build.SourcesDirectory)/out   # this directory is uploaded to pipeline artifacts, reddog and cloudvault. More info at https://aka.ms/obpipelines/artifacts
+
+        steps:
+        - task: GoTool@0
+          displayName: Use Go 1.14
+          inputs:
+            version: '1.14'
+            GOPATH: $(Agent.TempDirectory)
+        - task: Bash@3
+          displayName: ⚙️ Make ARO
+          inputs:
+            targetType: inline
+            script: |
+              mkdir -p $(Agent.TempDirectory)/src/github.com/Azure/
+              cp -rd $(Build.SourcesDirectory) $(Agent.TempDirectory)/src/github.com/Azure/ARO-RP
+              cd $(Agent.TempDirectory)/src/github.com/Azure/ARO-RP
+              ls
+              git rev-parse --short HEAD
+              git status --porcelain
+              make aro
+              mkdir -p $(ob_outputDirectory) 
+              cp aro $(ob_outputDirectory)/aro
+            workingDirectory: $(Build.SourcesDirectory)
diff --git a/.pipelines/onebranch/pipeline.buildrp.pullrequest.yml b/.pipelines/onebranch/pipeline.buildrp.pullrequest.yml
new file mode 100644
index 000000000..88099dbd6
--- /dev/null
+++ b/.pipelines/onebranch/pipeline.buildrp.pullrequest.yml
@@ -0,0 +1,125 @@
+#################################################################################
+#                               OneBranch Pipelines                             #
+# This pipeline was created by EasyStart from a sample located at:              #
+#   https://aka.ms/obpipelines/easystart/samples                                #
+# Documentation:  https://aka.ms/obpipelines                                    #
+# Yaml Schema:    https://aka.ms/obpipelines/yaml/schema                        #
+# Retail Tasks:   https://aka.ms/obpipelines/tasks                              #
+# Support:        https://aka.ms/onebranchsup                                   #
+#################################################################################
+
+trigger: none
+pr: none
+
+variables:
+  CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] # needed for onebranch.pipeline.version task https://aka.ms/obpipelines/versioning
+  LinuxContainerImage: cdpxlinux.azurecr.io/global/ubuntu-1804-all:5.0   # Docker image which is used to build the project https://aka.ms/obpipelines/containers
+  DEBIAN_FRONTEND: noninteractive
+
+resources:
+  repositories:
+  - repository: templates
+    type: git
+    name: OneBranch.Pipelines/GovernedTemplates
+    ref: refs/heads/main
+  - repository: rhado
+    type: git
+    name: ARO.Pipelines
+
+extends:
+  template: v2/OneBranch.NonOfficial.CrossPlat.yml@templates # https://aka.ms/obpipelines/templates
+  parameters:
+    globalSdl: # https://aka.ms/obpipelines/sdl
+      # tsa:
+      #  enabled: true # SDL results of non-official builds aren't uploaded to TSA by default.
+      # credscan:
+      #   suppressionsFile: $(Build.SourcesDirectory)\.config\CredScanSuppressions.json
+      policheck:
+        break: true # always break the build on policheck issues. You can disable it by setting to 'false'
+      suppression:
+        suppressionFile: $(Build.SourcesDirectory)\.gdn\.gdnsuppress
+        suppressionSet: default
+
+    stages:
+    - stage: Build_Ev2_Artifacts
+      jobs:
+      - job: Build_Ev2_Artifacts
+        pool:
+          type: linux
+
+        variables: # More settings at https://aka.ms/obpipelines/yaml/jobs
+          ob_outputDirectory: $(Build.SourcesDirectory)/out   # this directory is uploaded to pipeline artifacts, reddog and cloudvault. More info at https://aka.ms/obpipelines/artifacts
+
+        steps:
+        - checkout: rhado
+        - task: GoTool@0
+          displayName: Use Go 1.16.2
+          inputs:
+            version: 1.16.2
+        - task: Go@0
+          inputs:
+            command: custom
+            customCommand: run
+            arguments: . --config-path ./config/
+            workingDirectory: $(Build.SourcesDirectory)/ARO.Pipelines/ev2/generator/
+          displayName: ⚙️ Generate Ev2 Deployment Manifests
+        - task: Bash@3
+          displayName: ⚙️ Copy to ob_outputDirectory
+          inputs:
+            targetType: filePath
+            filePath: .pipelines/onebranch/scripts/deploymentpipeline.sh
+
+# https://onebranch.visualstudio.com/OneBranch/_wiki/wikis/OneBranch.wiki/4605/Artifacts
+
+    - stage: Build_Docker_Image
+      jobs:
+      - job: Build_Docker_Image
+        pool:
+          type: docker
+          os: linux
+        variables:
+          ob_git_checkout: true
+        steps:
+        - task: onebranch.pipeline.imagebuildinfo@1
+          displayName: Build Multi Stage Dockerfile
+          inputs:
+            repositoryName: aro-rp
+            dockerFileRelPath: ./Dockerfile.aro-multistage
+            dockerFileContextPath: ./
+            registry: cdpxlinux.azurecr.io
+            saveImageToPath: aro-rp.tar
+            buildkit: 1
+            enable_network: true
+            build_tag: $(Build.BuildNumber)
+
+
+    - stage: Build_ARO
+      jobs:
+      - job: Build_ARO
+        pool:
+          type: linux
+
+        variables: # More settings at https://aka.ms/obpipelines/yaml/jobs
+          ob_outputDirectory: $(Build.SourcesDirectory)/out   # this directory is uploaded to pipeline artifacts, reddog and cloudvault. More info at https://aka.ms/obpipelines/artifacts
+
+        steps:
+        - task: GoTool@0
+          displayName: Use Go 1.14
+          inputs:
+            version: '1.14'
+            GOPATH: $(Agent.TempDirectory)
+        - task: Bash@3
+          displayName: ⚙️ Make ARO
+          inputs:
+            targetType: inline
+            script: |
+              mkdir -p $(Agent.TempDirectory)/src/github.com/Azure/
+              cp -rd $(Build.SourcesDirectory) $(Agent.TempDirectory)/src/github.com/Azure/ARO-RP
+              cd $(Agent.TempDirectory)/src/github.com/Azure/ARO-RP
+              ls
+              git rev-parse --short HEAD
+              git status --porcelain
+              make aro
+              mkdir -p $(ob_outputDirectory) 
+              cp aro $(ob_outputDirectory)/aro
+            workingDirectory: $(Build.SourcesDirectory)
diff --git a/.pipelines/onebranch/pipeline.image.mirror.official.yml b/.pipelines/onebranch/pipeline.image.mirror.official.yml
new file mode 100644
index 000000000..9ca82eef6
--- /dev/null
+++ b/.pipelines/onebranch/pipeline.image.mirror.official.yml
@@ -0,0 +1,81 @@
+#################################################################################
+#                               OneBranch Pipelines                             #
+# This pipeline was created by EasyStart from a sample located at:              #
+#   https://aka.ms/obpipelines/easystart/samples                                #
+# Documentation:  https://aka.ms/obpipelines                                    #
+# Yaml Schema:    https://aka.ms/obpipelines/yaml/schema                        #
+# Retail Tasks:   https://aka.ms/obpipelines/tasks                              #
+# Support:        https://aka.ms/onebranchsup                                   #
+#################################################################################
+
+trigger: none
+pr: none
+
+parameters:
+- name: vsoDeployerBuildID
+  type: string
+  default: latest
+
+variables:
+  CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] # needed for onebranch.pipeline.version task https://aka.ms/obpipelines/versioning
+  LinuxContainerImage: centos:centos7   # Docker image which is used to build the project https://aka.ms/obpipelines/containers
+  DEBIAN_FRONTEND: noninteractive
+  vso-deployer-pipeline-id: 206753
+  vso-project-id: 5d69ab04-7ded-49dc-84d5-bbbcac4add8d
+
+resources:
+  repositories:
+  - repository: templates
+    type: git
+    name: OneBranch.Pipelines/GovernedTemplates
+    ref: refs/heads/main
+  - repository: rhado
+    type: git
+    name: ARO.Pipelines
+
+extends:
+  template: v2/OneBranch.Official.CrossPlat.yml@templates # https://aka.ms/obpipelines/templates
+  parameters:
+    globalSdl: # https://aka.ms/obpipelines/sdl
+      # tsa:
+      #  enabled: true # SDL results of non-official builds aren't uploaded to TSA by default.
+      # credscan:
+      #   suppressionsFile: $(Build.SourcesDirectory)\.config\CredScanSuppressions.json
+      policheck:
+        break: true # always break the build on policheck issues. You can disable it by setting to 'false'
+      suppression:
+        suppressionFile: $(Build.SourcesDirectory)\.gdn\.gdnsuppress
+        suppressionSet: default
+
+    stages:
+    - stage: Mirror_Images
+      variables:
+      - group: INT RP Service Secrets
+      jobs:
+      - job: Mirror_Images
+        timeoutInMinutes: 360
+        pool:
+          type: linux
+
+        variables: # More settings at https://aka.ms/obpipelines/yaml/jobs
+          ob_outputDirectory: $(Build.SourcesDirectory)/out   # this directory is uploaded to pipeline artifacts, reddog and cloudvault. More info at https://aka.ms/obpipelines/artifacts
+
+        steps:
+        - checkout: rhado
+        - task: DownloadPipelineArtifact@2
+          displayName: Download Deployer
+          inputs:
+            buildType: specific
+            project: $(vso-project-id)
+            pipeline: $(vso-deployer-pipeline-id)
+            ${{ if eq(parameters.vsoDeployerBuildID, 'latest') }}:
+              buildVersionToDownload: FromBranch
+              branchName: refs/heads/master
+            downloadType: specific
+            downloadPath: $(Build.SourcesDirectory)/deployer
+            artifact: drop_Build_ARO_Build_ARO
+        - task: Bash@3
+          displayName: ⚙️ Run Ev2 Manifest packaging
+          inputs:
+            targetType: filePath
+            filePath: .pipelines/onebranch/scripts/mirrorpipeline.sh
diff --git a/.pipelines/onebranch/pipeline.image.mirror.pullrequest.yml b/.pipelines/onebranch/pipeline.image.mirror.pullrequest.yml
new file mode 100644
index 000000000..f3b9c4b8a
--- /dev/null
+++ b/.pipelines/onebranch/pipeline.image.mirror.pullrequest.yml
@@ -0,0 +1,81 @@
+#################################################################################
+#                               OneBranch Pipelines                             #
+# This pipeline was created by EasyStart from a sample located at:              #
+#   https://aka.ms/obpipelines/easystart/samples                                #
+# Documentation:  https://aka.ms/obpipelines                                    #
+# Yaml Schema:    https://aka.ms/obpipelines/yaml/schema                        #
+# Retail Tasks:   https://aka.ms/obpipelines/tasks                              #
+# Support:        https://aka.ms/onebranchsup                                   #
+#################################################################################
+
+trigger: none
+pr: none
+
+parameters:
+- name: vsoDeployerBuildID
+  type: string
+  default: latest
+
+variables:
+  CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] # needed for onebranch.pipeline.version task https://aka.ms/obpipelines/versioning
+  LinuxContainerImage: centos:centos7   # Docker image which is used to build the project https://aka.ms/obpipelines/containers
+  DEBIAN_FRONTEND: noninteractive
+  vso-deployer-pipeline-id: 206753
+  vso-project-id: 5d69ab04-7ded-49dc-84d5-bbbcac4add8d
+
+resources:
+  repositories:
+  - repository: templates
+    type: git
+    name: OneBranch.Pipelines/GovernedTemplates
+    ref: refs/heads/main
+  - repository: rhado
+    type: git
+    name: ARO.Pipelines
+
+extends:
+  template: v2/OneBranch.NonOfficial.CrossPlat.yml@templates # https://aka.ms/obpipelines/templates
+  parameters:
+    globalSdl: # https://aka.ms/obpipelines/sdl
+      # tsa:
+      #  enabled: true # SDL results of non-official builds aren't uploaded to TSA by default.
+      # credscan:
+      #   suppressionsFile: $(Build.SourcesDirectory)\.config\CredScanSuppressions.json
+      policheck:
+        break: true # always break the build on policheck issues. You can disable it by setting to 'false'
+      suppression:
+        suppressionFile: $(Build.SourcesDirectory)\.gdn\.gdnsuppress
+        suppressionSet: default
+
+    stages:
+    - stage: Mirror_Images
+      variables:
+      - group: INT RP Service Secrets
+      jobs:
+      - job: Mirror_Images
+        timeoutInMinutes: 360
+        pool:
+          type: linux
+
+        variables: # More settings at https://aka.ms/obpipelines/yaml/jobs
+          ob_outputDirectory: $(Build.SourcesDirectory)/out   # this directory is uploaded to pipeline artifacts, reddog and cloudvault. More info at https://aka.ms/obpipelines/artifacts
+
+        steps:
+        - checkout: rhado
+        - task: DownloadPipelineArtifact@2
+          displayName: Download Deployer
+          inputs:
+            buildType: specific
+            project: $(vso-project-id)
+            pipeline: $(vso-deployer-pipeline-id)
+            ${{ if eq(parameters.vsoDeployerBuildID, 'latest') }}:
+              buildVersionToDownload: FromBranch
+              branchName: refs/heads/master
+            downloadType: specific
+            downloadPath: $(Build.SourcesDirectory)/deployer
+            artifact: drop_Build_ARO_Build_ARO
+        - task: Bash@3
+          displayName: ⚙️ Run Ev2 Manifest packaging
+          inputs:
+            targetType: filePath
+            filePath: .pipelines/onebranch/scripts/mirrorpipeline.sh
diff --git a/.pipelines/onebranch/scripts/deploymentpipeline.sh b/.pipelines/onebranch/scripts/deploymentpipeline.sh
new file mode 100644
index 000000000..dc521e3cf
--- /dev/null
+++ b/.pipelines/onebranch/scripts/deploymentpipeline.sh
@@ -0,0 +1,32 @@
+set -e
+
+echo "Creating required directories"
+
+mkdir -p $OB_OUTPUTDIRECTORY/ServiceGroupRoot/bin/
+mkdir -p $OB_OUTPUTDIRECTORY/ServiceGroupRoot/Parameters/
+mkdir -p $OB_OUTPUTDIRECTORY/Shell/
+
+echo "Downloading Crane"
+
+wget -O $OB_OUTPUTDIRECTORY/Shell/crane.tar.gz https://github.com/google/go-containerregistry/releases/download/v0.4.0/go-containerregistry_Linux_x86_64.tar.gz
+
+echo "Extracting Crane binaries"
+
+pushd $OB_OUTPUTDIRECTORY/Shell
+tar xzvf crane.tar.gz
+rm crane.tar.gz
+popd
+
+echo "Copying required files to ob_outputdirectory: ${OB_OUTPUTDIRECTORY}"
+
+tar -rvf ./ARO.Pipelines/ev2/generator/deployment.tar -C "$OB_OUTPUTDIRECTORY/Shell" $(cd $OB_OUTPUTDIRECTORY/Shell; echo *)
+tar -rvf ./ARO.Pipelines/ev2/generator/deployment.tar -C "./ARO.Pipelines/RP-Config" $(cd ./ARO.Pipelines/RP-Config; echo *)
+
+echo "Copy tar to ob_outputdirectory dir"
+cp -r ./ARO.Pipelines/ev2/Deployment/ServiceGroupRoot/ $OB_OUTPUTDIRECTORY/
+cp ./ARO.Pipelines/ev2/generator/deployment.tar $OB_OUTPUTDIRECTORY/ServiceGroupRoot/bin/
+
+echo "Listing the contents of dirs for debugging"
+ls $OB_OUTPUTDIRECTORY
+ls $OB_OUTPUTDIRECTORY/ServiceGroupRoot/
+ls $OB_OUTPUTDIRECTORY/ServiceGroupRoot/bin/
diff --git a/.pipelines/onebranch/scripts/mirrorpipeline.sh b/.pipelines/onebranch/scripts/mirrorpipeline.sh
new file mode 100644
index 000000000..106d5c197
--- /dev/null
+++ b/.pipelines/onebranch/scripts/mirrorpipeline.sh
@@ -0,0 +1,13 @@
+SERVICE_GROUP_ROOT=$BUILD_SOURCESDIRECTORY/ARO.Pipelines/ev2/Mirroring/ServiceGroupRoot
+EV2_BIN=$SERVICE_GROUP_ROOT/bin
+ARO_DIR=$BUILD_SOURCESDIRECTORY/deployer
+
+mkdir $OB_OUTPUTDIRECTORY
+
+cd $ARO_DIR
+tar -cvf $EV2_BIN/aro.tar aro
+cd $SERVICE_GROUP_ROOT/bin
+tar -rf $EV2_BIN/aro.tar mirror.sh
+rm mirror.sh
+
+cp -r $SERVICE_GROUP_ROOT $OB_OUTPUTDIRECTORY/ServiceGroupRoot/
diff --git a/cmd/aro/main.go b/cmd/aro/main.go
index b6dc42dca..7cac74bd3 100644
--- a/cmd/aro/main.go
+++ b/cmd/aro/main.go
@@ -62,7 +62,7 @@ func main() {
 		err = rp(ctx, log, audit)
 	case "portal":
 		checkArgs(1)
-		err = portal(ctx, log)
+		err = portal(ctx, log, audit)
 	case "operator":
 		checkArgs(2)
 		err = operator(ctx, log)
diff --git a/cmd/aro/portal.go b/cmd/aro/portal.go
index 975cfe90e..696999d64 100644
--- a/cmd/aro/portal.go
+++ b/cmd/aro/portal.go
@@ -24,7 +24,7 @@ import (
 	"github.com/Azure/ARO-RP/pkg/util/keyvault"
 )
 
-func portal(ctx context.Context, log *logrus.Entry) error {
+func portal(ctx context.Context, log *logrus.Entry, audit *logrus.Entry) error {
 	_env, err := env.NewCore(ctx, log)
 	if err != nil {
 		return err
@@ -166,7 +166,7 @@ func portal(ctx context.Context, log *logrus.Entry) error {
 
 	log.Print("listening")
 
-	p := pkgportal.NewPortal(_env, log.WithField("component", "portal"), log.WithField("component", "portal-access"), l, sshl, verifier, hostname, servingKey, servingCerts, clientID, clientKey, clientCerts, sessionKey, sshKey, groupIDs, elevatedGroupIDs, dbOpenShiftClusters, dbPortal, dialer)
+	p := pkgportal.NewPortal(_env, audit, log.WithField("component", "portal"), log.WithField("component", "portal-access"), l, sshl, verifier, hostname, servingKey, servingCerts, clientID, clientKey, clientCerts, sessionKey, sshKey, groupIDs, elevatedGroupIDs, dbOpenShiftClusters, dbPortal, dialer)
 
 	return p.Run(ctx)
 }
diff --git a/pkg/operator/controllers/monitoring/monitoring_controller.go b/pkg/operator/controllers/monitoring/monitoring_controller.go
index dc127c6c4..9fe9808c3 100644
--- a/pkg/operator/controllers/monitoring/monitoring_controller.go
+++ b/pkg/operator/controllers/monitoring/monitoring_controller.go
@@ -84,12 +84,6 @@ func (r *Reconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) {
 	// TODO(mj): Reconcile will eventually be receiving a ctx (https://github.com/kubernetes-sigs/controller-runtime/blob/7ef2da0bc161d823f084ad21ff5f9c9bd6b0cc39/pkg/reconcile/reconcile.go#L93)
 	ctx := context.TODO()
 
-	// check feature gates and if set to false remove any persistence
-	cluster, err := r.arocli.AroV1alpha1().Clusters().Get(ctx, arov1alpha1.SingletonClusterName, metav1.GetOptions{})
-	if err != nil {
-		return reconcile.Result{}, err
-	}
-
 	return reconcile.Result{}, retry.RetryOnConflict(retry.DefaultRetry, func() error {
 		cm, isCreate, err := r.monitoringConfigMap(ctx)
 		if err != nil {
@@ -111,28 +105,15 @@ func (r *Reconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) {
 		}
 
 		changed := false
-		switch cluster.Spec.Features.PersistentPrometheus {
-		case true:
-			// we are enabling persistence
-			if configData.PrometheusK8s.Retention != "15d" {
-				configData.PrometheusK8s.Retention = "15d"
-				changed = true
-			}
-			if configData.PrometheusK8s.VolumeClaimTemplate.Spec.Resources.Requests.Storage != "100Gi" {
-				configData.PrometheusK8s.VolumeClaimTemplate.Spec.Resources.Requests.Storage = "100Gi"
-				changed = true
-			}
-			// we are disabling persistence. We use omitempty on the struct to
-			// clean the fields
-		case false:
-			if configData.PrometheusK8s.Retention != "" {
-				configData.PrometheusK8s.Retention = ""
-				changed = true
-			}
-			if configData.PrometheusK8s.VolumeClaimTemplate.Spec.Resources.Requests.Storage != "" {
-				configData.PrometheusK8s.VolumeClaimTemplate.Spec.Resources.Requests.Storage = ""
-				changed = true
-			}
+		// we are disabling persistence. We use omitempty on the struct to
+		// clean the fields
+		if configData.PrometheusK8s.Retention != "" {
+			configData.PrometheusK8s.Retention = ""
+			changed = true
+		}
+		if configData.PrometheusK8s.VolumeClaimTemplate.Spec.Resources.Requests.Storage != "" {
+			configData.PrometheusK8s.VolumeClaimTemplate.Spec.Resources.Requests.Storage = ""
+			changed = true
 		}
 
 		if !isCreate && !changed {
@@ -152,10 +133,10 @@ func (r *Reconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) {
 		cm.Data["config.yaml"] = string(cmYaml)
 
 		if isCreate {
-			r.log.Infof("re-creating monitoring configmap. featureFlag %t", cluster.Spec.Features.PersistentPrometheus)
+			r.log.Infof("re-creating monitoring configmap. %s", monitoringName.Name)
 			_, err = r.kubernetescli.CoreV1().ConfigMaps(monitoringName.Namespace).Create(ctx, cm, metav1.CreateOptions{})
 		} else {
-			r.log.Infof("updating monitoring configmap. featureFlag %t", cluster.Spec.Features.PersistentPrometheus)
+			r.log.Infof("updating monitoring configmap. %s", monitoringName.Name)
 			_, err = r.kubernetescli.CoreV1().ConfigMaps(monitoringName.Namespace).Update(ctx, cm, metav1.UpdateOptions{})
 		}
 		return err
@@ -185,14 +166,14 @@ func (r *Reconciler) monitoringConfigMap(ctx context.Context) (*corev1.ConfigMap
 func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
 	r.log.Info("starting starting cluster monitoring controller")
 
-	monitoringConfigMapPredicate := predicate.NewPredicateFuncs(func(meta metav1.Object, object runtime.Object) bool {
-		return meta.GetName() == monitoringName.Name && meta.GetNamespace() == monitoringName.Namespace
-	})
-
 	aroClusterPredicate := predicate.NewPredicateFuncs(func(meta metav1.Object, object runtime.Object) bool {
 		return meta.GetName() == arov1alpha1.SingletonClusterName
 	})
 
+	monitoringConfigMapPredicate := predicate.NewPredicateFuncs(func(meta metav1.Object, object runtime.Object) bool {
+		return meta.GetName() == monitoringName.Name && meta.GetNamespace() == monitoringName.Namespace
+	})
+
 	return ctrl.NewControllerManagedBy(mgr).
 		For(&arov1alpha1.Cluster{}, builder.WithPredicates(aroClusterPredicate)).
 		// https://github.com/kubernetes-sigs/controller-runtime/issues/1173
diff --git a/pkg/operator/controllers/workaround/cleanprompvc.go b/pkg/operator/controllers/workaround/cleanprompvc.go
new file mode 100644
index 000000000..91218b1f9
--- /dev/null
+++ b/pkg/operator/controllers/workaround/cleanprompvc.go
@@ -0,0 +1,98 @@
+package workaround
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+// Clean the remaining PVCs in openshift-monitoring namespace.
+// These PVCs with labels: app=prometheus,prometheus=k8s are left
+// behind after switching back to use emptydir as persistent storage
+// for prometheus by disabling featureflag in monitoing controller.
+// This workaround is in effect for all clusters set to
+// have non-persistent prometheus.
+// The cleanup loop removes only up to 2 PVCs as this is the
+// production configuration at the time of the workaround release.
+
+import (
+	"context"
+
+	"github.com/ghodss/yaml"
+	"github.com/sirupsen/logrus"
+	"github.com/ugorji/go/codec"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/util/retry"
+
+	"github.com/Azure/ARO-RP/pkg/operator/controllers/monitoring"
+	"github.com/Azure/ARO-RP/pkg/util/version"
+)
+
+const (
+	prometheusLabels    = "app=prometheus,prometheus=k8s"
+	monitoringName      = "cluster-monitoring-config"
+	monitoringNamespace = "openshift-monitoring"
+)
+
+type cleanPromPVC struct {
+	log *logrus.Entry
+	cli kubernetes.Interface
+}
+
+func NewCleanFromPVCWorkaround(log *logrus.Entry, cli kubernetes.Interface) Workaround {
+	return &cleanPromPVC{
+		log: log,
+		cli: cli,
+	}
+}
+
+func (*cleanPromPVC) Name() string {
+	return "Clean prometheus PVC after disabling persistency"
+}
+
+func (c *cleanPromPVC) IsRequired(clusterVersion *version.Version) bool {
+	cm, err := c.cli.CoreV1().ConfigMaps(monitoringNamespace).Get(context.Background(), monitoringName, metav1.GetOptions{})
+	if err != nil {
+		return false
+	}
+
+	configDataJSON, err := yaml.YAMLToJSON([]byte(cm.Data["config.yaml"]))
+	if err != nil {
+		return false
+	}
+	var configData monitoring.Config
+	handle := new(codec.JsonHandle)
+	err = codec.NewDecoderBytes(configDataJSON, handle).Decode(&configData)
+	if err != nil {
+		return false
+	}
+
+	if configData.PrometheusK8s.Retention == "" && configData.PrometheusK8s.VolumeClaimTemplate.Spec.Resources.Requests.Storage == "" {
+		return true
+	}
+
+	return false
+}
+
+func (c *cleanPromPVC) Ensure(ctx context.Context) error {
+
+	err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
+		pvcList, err := c.cli.CoreV1().PersistentVolumeClaims(monitoringNamespace).List(ctx, metav1.ListOptions{LabelSelector: prometheusLabels})
+		if err != nil {
+			return err
+		}
+
+		for _, pvc := range pvcList.Items {
+			err = c.cli.CoreV1().PersistentVolumeClaims(monitoringNamespace).Delete(ctx, pvc.Name, metav1.DeleteOptions{})
+			if err != nil {
+				return err
+			}
+		}
+
+		return nil
+	})
+
+	return err
+}
+
+func (c *cleanPromPVC) Remove(ctx context.Context) error {
+	return nil
+}
diff --git a/pkg/operator/controllers/workaround/workaround_controller.go b/pkg/operator/controllers/workaround/workaround_controller.go
index 6f9832f09..ec480bad1 100644
--- a/pkg/operator/controllers/workaround/workaround_controller.go
+++ b/pkg/operator/controllers/workaround/workaround_controller.go
@@ -44,7 +44,7 @@ func NewReconciler(log *logrus.Entry, kubernetescli kubernetes.Interface, config
 		configcli:     configcli,
 		arocli:        arocli,
 		restConfig:    restConfig,
-		workarounds:   []Workaround{NewSystemReserved(log, mcocli, dh), NewIfReload(log, kubernetescli)},
+		workarounds:   []Workaround{NewSystemReserved(log, mcocli, dh), NewIfReload(log, kubernetescli), NewCleanFromPVCWorkaround(log, kubernetescli)},
 		log:           log,
 	}
 }
diff --git a/pkg/portal/kubeconfig/kubeconfig.go b/pkg/portal/kubeconfig/kubeconfig.go
index af4f2951c..503e3980a 100644
--- a/pkg/portal/kubeconfig/kubeconfig.go
+++ b/pkg/portal/kubeconfig/kubeconfig.go
@@ -22,6 +22,7 @@ import (
 	"github.com/Azure/ARO-RP/pkg/api"
 	"github.com/Azure/ARO-RP/pkg/api/validate"
 	"github.com/Azure/ARO-RP/pkg/database"
+	"github.com/Azure/ARO-RP/pkg/env"
 	"github.com/Azure/ARO-RP/pkg/portal/middleware"
 	"github.com/Azure/ARO-RP/pkg/portal/util/clientcache"
 	"github.com/Azure/ARO-RP/pkg/proxy"
@@ -49,6 +50,8 @@ type kubeconfig struct {
 }
 
 func New(baseLog *logrus.Entry,
+	audit *logrus.Entry,
+	env env.Core,
 	baseAccessLog *logrus.Entry,
 	servingCert *x509.Certificate,
 	elevatedGroupIDs []string,
@@ -83,7 +86,7 @@ func New(baseLog *logrus.Entry,
 
 	bearerAuthenticatedRouter := unauthenticatedRouter.NewRoute().Subrouter()
 	bearerAuthenticatedRouter.Use(middleware.Bearer(k.dbPortal))
-	bearerAuthenticatedRouter.Use(middleware.Log(k.baseAccessLog))
+	bearerAuthenticatedRouter.Use(middleware.Log(env, audit, k.baseAccessLog))
 
 	bearerAuthenticatedRouter.PathPrefix("/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.redhatopenshift/openshiftclusters/{resourceName}/kubeconfig/proxy/").Handler(rp)
 
diff --git a/pkg/portal/middleware/aad.go b/pkg/portal/middleware/aad.go
index 33efddc53..160bf1603 100644
--- a/pkg/portal/middleware/aad.go
+++ b/pkg/portal/middleware/aad.go
@@ -106,6 +106,7 @@ type aad struct {
 }
 
 func NewAAD(log *logrus.Entry,
+	audit *logrus.Entry,
 	env env.Core,
 	baseAccessLog *logrus.Entry,
 	hostname string,
@@ -151,7 +152,7 @@ func NewAAD(log *logrus.Entry,
 	a.store.Options.HttpOnly = true
 	a.store.Options.SameSite = http.SameSiteLaxMode
 
-	unauthenticatedRouter.NewRoute().Methods(http.MethodGet).Path("/callback").Handler(Log(baseAccessLog)(http.HandlerFunc(a.callback)))
+	unauthenticatedRouter.NewRoute().Methods(http.MethodGet).Path("/callback").Handler(Log(env, audit, baseAccessLog)(http.HandlerFunc(a.callback)))
 
 	return a, nil
 }
diff --git a/pkg/portal/middleware/log.go b/pkg/portal/middleware/log.go
index 945b5f3e3..83181f614 100644
--- a/pkg/portal/middleware/log.go
+++ b/pkg/portal/middleware/log.go
@@ -5,6 +5,7 @@ package middleware
 
 import (
 	"bufio"
+	"fmt"
 	"io"
 	"net"
 	"net/http"
@@ -12,7 +13,9 @@ import (
 
 	"github.com/sirupsen/logrus"
 
+	"github.com/Azure/ARO-RP/pkg/env"
 	utillog "github.com/Azure/ARO-RP/pkg/util/log"
+	"github.com/Azure/ARO-RP/pkg/util/log/audit"
 )
 
 type logResponseWriter struct {
@@ -50,7 +53,7 @@ func (rc *logReadCloser) Read(b []byte) (int, error) {
 	return n, err
 }
 
-func Log(baseLog *logrus.Entry) func(http.Handler) http.Handler {
+func Log(env env.Core, auditLog, baseLog *logrus.Entry) func(http.Handler) http.Handler {
 	return func(h http.Handler) http.Handler {
 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			t := time.Now()
@@ -73,16 +76,64 @@ func Log(baseLog *logrus.Entry) func(http.Handler) http.Handler {
 			})
 			log.Print("read request")
 
+			auditEntry := auditLog.WithFields(logrus.Fields{
+				audit.MetadataAdminOperation:  true,
+				audit.MetadataCreatedTime:     time.Now().UTC().Format(time.RFC3339),
+				audit.MetadataLogKind:         audit.IFXAuditLogKind,
+				audit.MetadataSource:          audit.SourceAdminPortal,
+				audit.EnvKeyAppID:             audit.SourceAdminPortal,
+				audit.EnvKeyCloudRole:         audit.CloudRoleRP,
+				audit.EnvKeyEnvironment:       env.Environment().Name,
+				audit.EnvKeyHostname:          env.Hostname(),
+				audit.EnvKeyLocation:          env.Location(),
+				audit.PayloadKeyCategory:      audit.CategoryResourceManagement,
+				audit.PayloadKeyOperationName: fmt.Sprintf("%s %s", r.Method, r.URL.Path),
+				audit.PayloadKeyCallerIdentities: []audit.CallerIdentity{
+					{
+						CallerIdentityType:  audit.CallerIdentityTypeUsername,
+						CallerIdentityValue: username,
+						CallerIPAddress:     r.RemoteAddr,
+					},
+				},
+				audit.PayloadKeyTargetResources: []audit.TargetResource{
+					{
+						TargetResourceName: r.URL.Path,
+						TargetResourceType: auditTargetResourceType(r),
+					},
+				},
+			})
+
 			defer func() {
+				statusCode := w.(*logResponseWriter).statusCode
 				log.WithFields(logrus.Fields{
 					"body_read_bytes":      r.Body.(*logReadCloser).bytes,
 					"body_written_bytes":   w.(*logResponseWriter).bytes,
 					"duration":             time.Since(t).Seconds(),
-					"response_status_code": w.(*logResponseWriter).statusCode,
+					"response_status_code": statusCode,
 				}).Print("sent response")
+
+				resultType := audit.ResultTypeSuccess
+				if statusCode >= http.StatusBadRequest {
+					resultType = audit.ResultTypeFail
+				}
+
+				auditEntry.WithFields(logrus.Fields{
+					audit.PayloadKeyResult: audit.Result{
+						ResultType:        resultType,
+						ResultDescription: fmt.Sprintf("Status code: %d", statusCode),
+					},
+				}).Info(audit.DefaultLogMessage)
 			}()
 
 			h.ServeHTTP(w, r)
 		})
 	}
 }
+
+func auditTargetResourceType(r *http.Request) string {
+	if matches := utillog.RXTolerantSubResourceID.FindStringSubmatch(r.URL.Path); matches != nil {
+		return matches[len(matches)-1]
+	}
+
+	return ""
+}
diff --git a/pkg/portal/portal.go b/pkg/portal/portal.go
index b2147faee..4d5073992 100644
--- a/pkg/portal/portal.go
+++ b/pkg/portal/portal.go
@@ -38,6 +38,7 @@ type Runnable interface {
 
 type portal struct {
 	env           env.Core
+	audit         *logrus.Entry
 	log           *logrus.Entry
 	baseAccessLog *logrus.Entry
 	l             net.Listener
@@ -67,6 +68,7 @@ type portal struct {
 }
 
 func NewPortal(env env.Core,
+	audit *logrus.Entry,
 	log *logrus.Entry,
 	baseAccessLog *logrus.Entry,
 	l net.Listener,
@@ -87,6 +89,7 @@ func NewPortal(env env.Core,
 	dialer proxy.Dialer) Runnable {
 	return &portal{
 		env:           env,
+		audit:         audit,
 		log:           log,
 		baseAccessLog: baseAccessLog,
 		l:             l,
@@ -160,14 +163,14 @@ func (p *portal) Run(ctx context.Context) error {
 	allGroups := append([]string{}, p.groupIDs...)
 	allGroups = append(allGroups, p.elevatedGroupIDs...)
 
-	p.aad, err = middleware.NewAAD(p.log, p.env, p.baseAccessLog, p.hostname, p.sessionKey, p.clientID, p.clientKey, p.clientCerts, allGroups, unauthenticatedRouter, p.verifier)
+	p.aad, err = middleware.NewAAD(p.log, p.audit, p.env, p.baseAccessLog, p.hostname, p.sessionKey, p.clientID, p.clientKey, p.clientCerts, allGroups, unauthenticatedRouter, p.verifier)
 	if err != nil {
 		return err
 	}
 
 	aadAuthenticatedRouter := r.NewRoute().Subrouter()
 	aadAuthenticatedRouter.Use(p.aad.AAD)
-	aadAuthenticatedRouter.Use(middleware.Log(p.baseAccessLog))
+	aadAuthenticatedRouter.Use(middleware.Log(p.env, p.audit, p.baseAccessLog))
 	aadAuthenticatedRouter.Use(p.aad.Redirect)
 	aadAuthenticatedRouter.Use(csrf.Protect(p.sessionKey, csrf.SameSite(csrf.SameSiteStrictMode), csrf.MaxAge(0)))
 
@@ -183,7 +186,7 @@ func (p *portal) Run(ctx context.Context) error {
 		return err
 	}
 
-	kubeconfig.New(p.log, p.baseAccessLog, p.servingCerts[0], p.elevatedGroupIDs, p.dbOpenShiftClusters, p.dbPortal, p.dialer, aadAuthenticatedRouter, unauthenticatedRouter)
+	kubeconfig.New(p.log, p.audit, p.env, p.baseAccessLog, p.servingCerts[0], p.elevatedGroupIDs, p.dbOpenShiftClusters, p.dbPortal, p.dialer, aadAuthenticatedRouter, unauthenticatedRouter)
 
 	prometheus.New(p.log, p.dbOpenShiftClusters, p.dialer, aadAuthenticatedRouter)
 
@@ -199,7 +202,7 @@ func (p *portal) Run(ctx context.Context) error {
 }
 
 func (p *portal) unauthenticatedRoutes(r *mux.Router) {
-	logger := middleware.Log(p.baseAccessLog)
+	logger := middleware.Log(p.env, p.audit, p.baseAccessLog)
 
 	r.NewRoute().Methods(http.MethodGet).Path("/healthz/ready").Handler(logger(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})))
 }
diff --git a/pkg/util/log/log.go b/pkg/util/log/log.go
index c255c59fa..2a47ee109 100644
--- a/pkg/util/log/log.go
+++ b/pkg/util/log/log.go
@@ -34,6 +34,8 @@ var (
 	RXAdminProvider = regexp.MustCompile(`^/admin/providers/([^/]+)/([^/]+)$`)
 
 	RXTolerantResourceID = regexp.MustCompile(`(?i)^(?:/admin)?/subscriptions/([^/]+)(?:/resourceGroups/([^/]+)(?:/providers/([^/]+)/([^/]+)(?:/([^/]+))?)?)?`)
+
+	RXTolerantSubResourceID = regexp.MustCompile(`(?i)^(?:/admin)?/subscriptions/([^/]+)(?:/resourceGroups/([^/]+)(?:/providers/([^/]+)/([^/]+)/([^/]+)(?:/([^/]+))?)?)?`)
 )
 
 func getBaseLogger() *logrus.Logger {


Test patch:
diff --git a/pkg/frontend/security_test.go b/pkg/frontend/security_test.go
index 83530ec76..d62da729e 100644
--- a/pkg/frontend/security_test.go
+++ b/pkg/frontend/security_test.go
@@ -495,6 +495,8 @@ func TestSecurity(t *testing.T) {
 		},
 	} {
 		t.Run(tt.name, func(t *testing.T) {
+			defer auditHook.Reset()
+
 			tlsConfig := &tls.Config{
 				RootCAs: pool,
 			}
@@ -527,7 +529,6 @@ func TestSecurity(t *testing.T) {
 			}
 
 			testlog.AssertAuditPayloads(t, auditHook, tt.wantAuditPayloads)
-			auditHook.Entries = []logrus.Entry{}
 		})
 	}
 }
diff --git a/pkg/operator/controllers/monitoring/monitoring_controller_test.go b/pkg/operator/controllers/monitoring/monitoring_controller_test.go
index 903854e21..85c4c70a3 100644
--- a/pkg/operator/controllers/monitoring/monitoring_controller_test.go
+++ b/pkg/operator/controllers/monitoring/monitoring_controller_test.go
@@ -49,14 +49,7 @@ func TestReconcileMonitoringConfig(t *testing.T) {
 				}
 			},
 			wantConfig: `
-prometheusK8s:
-  retention: 15d
-  volumeClaimTemplate:
-    spec:
-      resources:
-        requests:
-          storage: 100Gi
-`,
+{}`,
 		},
 		{
 			name: "ConfigMap does not have data",
@@ -77,15 +70,7 @@ prometheusK8s:
 					jsonHandle: new(codec.JsonHandle),
 				}
 			},
-			wantConfig: `
-prometheusK8s:
-  retention: 15d
-  volumeClaimTemplate:
-    spec:
-      resources:
-        requests:
-          storage: 100Gi
-`,
+			wantConfig: ``,
 		},
 		{
 			name: "empty config.yaml",
@@ -109,15 +94,7 @@ prometheusK8s:
 					jsonHandle: new(codec.JsonHandle),
 				}
 			},
-			wantConfig: `
-prometheusK8s:
-  retention: 15d
-  volumeClaimTemplate:
-    spec:
-      resources:
-        requests:
-          storage: 100Gi
-`,
+			wantConfig: ``,
 		},
 		{
 			name: "settings restored to default and extra fields are preserved",
@@ -153,12 +130,8 @@ prometheusK8s:
 			},
 			wantConfig: `
 prometheusK8s:
-  retention: 15d
   volumeClaimTemplate:
     spec:
-      resources:
-        requests:
-          storage: 100Gi
       storageClassName: fast
       volumeMode: Filesystem
 `,
@@ -193,13 +166,6 @@ alertmanagerMain:
 alertmanagerMain:
   nodeSelector:
     foo: bar
-prometheusK8s:
-  retention: 15d
-  volumeClaimTemplate:
-    spec:
-      resources:
-        requests:
-          storage: 100Gi
 `,
 		},
 		{
diff --git a/pkg/operator/controllers/workaround/cleanprompvc_test.go b/pkg/operator/controllers/workaround/cleanprompvc_test.go
new file mode 100644
index 000000000..a8ccfdff4
--- /dev/null
+++ b/pkg/operator/controllers/workaround/cleanprompvc_test.go
@@ -0,0 +1,154 @@
+package workaround
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes/fake"
+
+	utillog "github.com/Azure/ARO-RP/pkg/util/log"
+)
+
+func TestCleanPromPVCEnsure(t *testing.T) {
+
+	pvc0 := corev1.PersistentVolumeClaim{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "prometheus-k8s-db-prometheus-k8s-00",
+			Namespace: "openshift-monitoring",
+			Labels: map[string]string{
+				"app":        "prometheus",
+				"prometheus": "k8s",
+			},
+		},
+	}
+
+	pvc1 := corev1.PersistentVolumeClaim{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "prometheus-k8s-db-prometheus-k8s-0",
+			Namespace: "openshift-monitoring",
+			Labels: map[string]string{
+				"app":        "prometheus",
+				"prometheus": "k8s",
+			},
+		},
+	}
+
+	pvc2 := corev1.PersistentVolumeClaim{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "prometheus-k8s-db-prometheus-k8s-1",
+			Namespace: "openshift-monitoring",
+			Labels: map[string]string{
+				"app":        "prometheus",
+				"prometheus": "k8s",
+			},
+		},
+	}
+
+	pvc3 := corev1.PersistentVolumeClaim{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "dummy",
+			Namespace: "openshift-monitoring",
+		},
+	}
+
+	tests := []struct {
+		name       string
+		cli        *fake.Clientset
+		wantPVCNum int
+		wantErr    error
+	}{
+		{
+			name:       "Should delete the prometheus PVCs",
+			cli:        fake.NewSimpleClientset(&pvc1, &pvc2, &pvc3),
+			wantPVCNum: 1,
+			wantErr:    nil,
+		},
+		{
+			name:       "Should not delete the prometheus PVCs, too many items",
+			cli:        fake.NewSimpleClientset(&pvc1, &pvc2, &pvc3, &pvc0),
+			wantPVCNum: 1,
+			wantErr:    nil,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			w := NewCleanFromPVCWorkaround(utillog.GetLogger(), tt.cli)
+			err := w.Ensure(context.Background())
+			if err != tt.wantErr {
+				t.Fatalf("Unexpected error\nwant: %v\ngot: %v", tt.wantErr, err)
+			}
+
+			pvcList, err := tt.cli.CoreV1().PersistentVolumeClaims(monitoringNamespace).List(context.Background(), metav1.ListOptions{})
+			if err != nil {
+				t.Fatalf("Unexpected error during list of PVCs: %v", err)
+			}
+			if len(pvcList.Items) != tt.wantPVCNum {
+				t.Fatalf("Unexpected number of PVCs\nwant: %d\ngot: %d", tt.wantPVCNum, len(pvcList.Items))
+			}
+		})
+	}
+}
+
+func TestCleanPromPVCIsRequired(t *testing.T) {
+	newKubernetesCli := func(config string) *fake.Clientset {
+		configMap := corev1.ConfigMap{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      monitoringName,
+				Namespace: monitoringNamespace,
+			},
+			Data: make(map[string]string),
+		}
+
+		configMap.Data["config.yaml"] = config
+
+		return fake.NewSimpleClientset(&configMap)
+	}
+
+	tests := []struct {
+		name         string
+		kcli         *fake.Clientset
+		wantRequired bool
+	}{
+		{
+			name: "Should not be required, persistent set true",
+			kcli: newKubernetesCli(`prometheusK8s:
+  retention: 15d
+  volumeClaimTemplate:
+    spec:
+      resources:
+        requests:
+          storage: 100Gi
+            `),
+			wantRequired: false,
+		},
+		{
+			name: "Should be required, persistent set to false",
+			kcli: newKubernetesCli(`prometheusK8s:
+  retention: ""
+  volumeClaimTemplate:
+    spec:
+      resources:
+        requests:
+          storage: ""
+            `),
+			wantRequired: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			w := NewCleanFromPVCWorkaround(utillog.GetLogger(), tt.kcli)
+			required := w.IsRequired(nil)
+			if required != tt.wantRequired {
+				t.Fatalf("Unexpected workaroud required result\nwant: %t\ngot: %t", tt.wantRequired, required)
+			}
+
+		})
+	}
+}
diff --git a/pkg/portal/kubeconfig/kubeconfig_test.go b/pkg/portal/kubeconfig/kubeconfig_test.go
index 5022d6a6e..782f291b3 100644
--- a/pkg/portal/kubeconfig/kubeconfig_test.go
+++ b/pkg/portal/kubeconfig/kubeconfig_test.go
@@ -12,15 +12,17 @@ import (
 	"reflect"
 	"testing"
 
+	"github.com/Azure/go-autorest/autorest/azure"
 	"github.com/golang/mock/gomock"
 	"github.com/gorilla/mux"
-	"github.com/sirupsen/logrus"
 
 	"github.com/Azure/ARO-RP/pkg/api"
 	"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
 	"github.com/Azure/ARO-RP/pkg/portal/middleware"
 	"github.com/Azure/ARO-RP/pkg/portal/util/responsewriter"
+	mock_env "github.com/Azure/ARO-RP/pkg/util/mocks/env"
 	testdatabase "github.com/Azure/ARO-RP/test/database"
+	testlog "github.com/Azure/ARO-RP/test/util/log"
 )
 
 func TestNew(t *testing.T) {
@@ -136,9 +138,17 @@ func TestNew(t *testing.T) {
 			ctrl := gomock.NewController(t)
 			defer ctrl.Finish()
 
+			_env := mock_env.NewMockInterface(ctrl)
+			_env.EXPECT().Environment().AnyTimes().Return(&azure.PublicCloud)
+			_env.EXPECT().Hostname().AnyTimes().Return("testhost")
+			_env.EXPECT().Location().AnyTimes().Return("eastus")
+
 			aadAuthenticatedRouter := &mux.Router{}
 
-			k := New(logrus.NewEntry(logrus.StandardLogger()), nil, servingCert, elevatedGroupIDs, nil, dbPortal, nil, aadAuthenticatedRouter, &mux.Router{})
+			_, audit := testlog.NewAudit()
+			_, baseLog := testlog.New()
+			_, baseAccessLog := testlog.New()
+			k := New(baseLog, audit, _env, baseAccessLog, servingCert, elevatedGroupIDs, nil, dbPortal, nil, aadAuthenticatedRouter, &mux.Router{})
 
 			k.newToken = func() string { return password }
 
diff --git a/pkg/portal/kubeconfig/proxy_test.go b/pkg/portal/kubeconfig/proxy_test.go
index 409eb1757..b19f7c71e 100644
--- a/pkg/portal/kubeconfig/proxy_test.go
+++ b/pkg/portal/kubeconfig/proxy_test.go
@@ -15,18 +15,20 @@ import (
 	"net/http/httputil"
 	"testing"
 
+	"github.com/Azure/go-autorest/autorest/azure"
 	"github.com/golang/mock/gomock"
 	"github.com/gorilla/mux"
-	"github.com/sirupsen/logrus"
 	clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1"
 
 	"github.com/Azure/ARO-RP/pkg/api"
 	"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
 	"github.com/Azure/ARO-RP/pkg/portal/util/responsewriter"
+	mock_env "github.com/Azure/ARO-RP/pkg/util/mocks/env"
 	mock_proxy "github.com/Azure/ARO-RP/pkg/util/mocks/proxy"
 	utiltls "github.com/Azure/ARO-RP/pkg/util/tls"
 	testdatabase "github.com/Azure/ARO-RP/test/database"
 	"github.com/Azure/ARO-RP/test/util/listener"
+	testlog "github.com/Azure/ARO-RP/test/util/log"
 )
 
 // fakeServer returns a test listener for an HTTPS server which validates its
@@ -381,6 +383,11 @@ func TestProxy(t *testing.T) {
 			ctrl := gomock.NewController(t)
 			defer ctrl.Finish()
 
+			_env := mock_env.NewMockInterface(ctrl)
+			_env.EXPECT().Environment().AnyTimes().Return(&azure.PublicCloud)
+			_env.EXPECT().Hostname().AnyTimes().Return("testhost")
+			_env.EXPECT().Location().AnyTimes().Return("eastus")
+
 			dialer := mock_proxy.NewMockDialer(ctrl)
 			if tt.mocks != nil {
 				tt.mocks(dialer)
@@ -388,7 +395,10 @@ func TestProxy(t *testing.T) {
 
 			unauthenticatedRouter := &mux.Router{}
 
-			k := New(logrus.NewEntry(logrus.StandardLogger()), logrus.NewEntry(logrus.StandardLogger()), nil, nil, dbOpenShiftClusters, dbPortal, dialer, &mux.Router{}, unauthenticatedRouter)
+			_, audit := testlog.NewAudit()
+			_, baseLog := testlog.New()
+			_, baseAccessLog := testlog.New()
+			k := New(baseLog, audit, _env, baseAccessLog, nil, nil, dbOpenShiftClusters, dbPortal, dialer, &mux.Router{}, unauthenticatedRouter)
 
 			k.newToken = func() string { return token }
 
diff --git a/pkg/portal/middleware/aad_test.go b/pkg/portal/middleware/aad_test.go
index 740823432..44fe3ce6c 100644
--- a/pkg/portal/middleware/aad_test.go
+++ b/pkg/portal/middleware/aad_test.go
@@ -23,12 +23,12 @@ import (
 	"github.com/golang/mock/gomock"
 	"github.com/gorilla/mux"
 	"github.com/gorilla/securecookie"
-	"github.com/sirupsen/logrus"
 	"golang.org/x/oauth2"
 
 	mock_env "github.com/Azure/ARO-RP/pkg/util/mocks/env"
 	"github.com/Azure/ARO-RP/pkg/util/roundtripper"
 	utiltls "github.com/Azure/ARO-RP/pkg/util/tls"
+	testlog "github.com/Azure/ARO-RP/test/util/log"
 )
 
 var (
@@ -81,7 +81,7 @@ func (c noopClaims) Claims(v interface{}) error {
 }
 
 func TestNewAAD(t *testing.T) {
-	_, err := NewAAD(nil, nil, nil, "", nil, "", nil, nil, nil, nil, nil)
+	_, err := NewAAD(nil, nil, nil, nil, "", nil, "", nil, nil, nil, nil, nil)
 	if err.Error() != "invalid sessionKey" {
 		t.Error(err)
 	}
@@ -162,7 +162,10 @@ func TestAAD(t *testing.T) {
 			env.EXPECT().IsDevelopmentMode().AnyTimes().Return(false)
 			env.EXPECT().TenantID().AnyTimes().Return("")
 
-			a, err := NewAAD(logrus.NewEntry(logrus.StandardLogger()), env, logrus.NewEntry(logrus.StandardLogger()), "", make([]byte, 32), "", nil, nil, nil, mux.NewRouter(), nil)
+			_, audit := testlog.NewAudit()
+			_, baseLog := testlog.New()
+			_, baseAccessLog := testlog.New()
+			a, err := NewAAD(baseLog, audit, env, baseAccessLog, "", make([]byte, 32), "", nil, nil, nil, mux.NewRouter(), nil)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -258,7 +261,10 @@ func TestRedirect(t *testing.T) {
 			env.EXPECT().IsDevelopmentMode().AnyTimes().Return(false)
 			env.EXPECT().TenantID().AnyTimes().Return("")
 
-			a, err := NewAAD(logrus.NewEntry(logrus.StandardLogger()), env, logrus.NewEntry(logrus.StandardLogger()), "", make([]byte, 32), "", nil, nil, nil, mux.NewRouter(), nil)
+			_, audit := testlog.NewAudit()
+			_, baseLog := testlog.New()
+			_, baseAccessLog := testlog.New()
+			a, err := NewAAD(baseLog, audit, env, baseAccessLog, "", make([]byte, 32), "", nil, nil, nil, mux.NewRouter(), nil)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -374,7 +380,10 @@ func TestLogout(t *testing.T) {
 			env.EXPECT().IsDevelopmentMode().AnyTimes().Return(false)
 			env.EXPECT().TenantID().AnyTimes().Return("")
 
-			a, err := NewAAD(logrus.NewEntry(logrus.StandardLogger()), env, logrus.NewEntry(logrus.StandardLogger()), "", make([]byte, 32), "", nil, nil, nil, mux.NewRouter(), nil)
+			_, audit := testlog.NewAudit()
+			_, baseLog := testlog.New()
+			_, baseAccessLog := testlog.New()
+			a, err := NewAAD(baseLog, audit, env, baseAccessLog, "", make([]byte, 32), "", nil, nil, nil, mux.NewRouter(), nil)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -736,7 +745,10 @@ func TestCallback(t *testing.T) {
 			env.EXPECT().IsDevelopmentMode().AnyTimes().Return(false)
 			env.EXPECT().TenantID().AnyTimes().Return("")
 
-			a, err := NewAAD(logrus.NewEntry(logrus.StandardLogger()), env, logrus.NewEntry(logrus.StandardLogger()), "", make([]byte, 32), clientID, clientkey, clientcerts, groups, mux.NewRouter(), tt.verifier)
+			_, audit := testlog.NewAudit()
+			_, baseLog := testlog.New()
+			_, baseAccessLog := testlog.New()
+			a, err := NewAAD(baseLog, audit, env, baseAccessLog, "", make([]byte, 32), clientID, clientkey, clientcerts, groups, mux.NewRouter(), tt.verifier)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -854,8 +866,10 @@ func TestClientAssertion(t *testing.T) {
 	env.EXPECT().TenantID().AnyTimes().Return("")
 
 	clientID := "00000000-0000-0000-0000-000000000000"
-
-	a, err := NewAAD(logrus.NewEntry(logrus.StandardLogger()), env, logrus.NewEntry(logrus.StandardLogger()), "", make([]byte, 32), clientID, clientkey, clientcerts, nil, mux.NewRouter(), nil)
+	_, audit := testlog.NewAudit()
+	_, baseLog := testlog.New()
+	_, baseAccessLog := testlog.New()
+	a, err := NewAAD(baseLog, audit, env, baseAccessLog, "", make([]byte, 32), clientID, clientkey, clientcerts, nil, mux.NewRouter(), nil)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/pkg/portal/middleware/log_test.go b/pkg/portal/middleware/log_test.go
index 1b8d2af83..153ace4e7 100644
--- a/pkg/portal/middleware/log_test.go
+++ b/pkg/portal/middleware/log_test.go
@@ -9,18 +9,31 @@ import (
 	"io"
 	"net/http"
 	"net/http/httptest"
+	"net/url"
 	"strings"
 	"testing"
 
+	"github.com/Azure/go-autorest/autorest/azure"
+	"github.com/golang/mock/gomock"
 	"github.com/onsi/gomega"
 	"github.com/onsi/gomega/types"
 	"github.com/sirupsen/logrus"
 
+	"github.com/Azure/ARO-RP/pkg/util/log/audit"
+	mock_env "github.com/Azure/ARO-RP/pkg/util/mocks/env"
 	testlog "github.com/Azure/ARO-RP/test/util/log"
 )
 
 func TestLog(t *testing.T) {
 	h, log := testlog.New()
+	ah, auditLog := testlog.NewAudit()
+
+	controller := gomock.NewController(t)
+	defer controller.Finish()
+	_env := mock_env.NewMockInterface(controller)
+	_env.EXPECT().Environment().AnyTimes().Return(&azure.PublicCloud)
+	_env.EXPECT().Hostname().AnyTimes().Return("testhost")
+	_env.EXPECT().Location().AnyTimes().Return("eastus")
 
 	ctx := context.WithValue(context.Background(), ContextKeyUsername, "username")
 	r, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://localhost/", strings.NewReader("body"))
@@ -32,7 +45,8 @@ func TestLog(t *testing.T) {
 
 	w := httptest.NewRecorder()
 
-	Log(log)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+	// chain a custom handler with the Log middleware to mutate the request
+	Log(_env, auditLog, log)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		r.URL = nil // mutate the request
 
 		_ = w.(http.Hijacker) // must implement http.Hijacker
@@ -76,7 +90,115 @@ func TestLog(t *testing.T) {
 		t.Error(err)
 	}
 
+	// only one audit log is associated with the one POST request
+	expectedAudit := []*audit.Payload{
+		{
+			EnvVer:               audit.IFXAuditVersion,
+			EnvName:              audit.IFXAuditName,
+			EnvFlags:             257,
+			EnvAppID:             audit.SourceAdminPortal,
+			EnvCloudName:         _env.Environment().Name,
+			EnvCloudRole:         audit.CloudRoleRP,
+			EnvCloudRoleInstance: _env.Hostname(),
+			EnvCloudEnvironment:  _env.Environment().Name,
+			EnvCloudLocation:     _env.Location(),
+			EnvCloudVer:          audit.IFXAuditCloudVer,
+			CallerIdentities: []audit.CallerIdentity{
+				{
+					CallerIdentityType:  audit.CallerIdentityTypeUsername,
+					CallerIdentityValue: "username",
+					CallerIPAddress:     "127.0.0.1:1234",
+				},
+			},
+			Category:      "ResourceManagement",
+			OperationName: "POST /",
+			Result: audit.Result{
+				ResultType:        "Success",
+				ResultDescription: "Status code: 200",
+			},
+			TargetResources: []audit.TargetResource{
+				{
+					TargetResourceType: "",
+					TargetResourceName: "/",
+				},
+			},
+		},
+	}
+	testlog.AssertAuditPayloads(t, ah, expectedAudit)
+
 	for _, e := range h.Entries {
 		fmt.Println(e)
 	}
+
+}
+
+func TestAuditTargetResourceData(t *testing.T) {
+	var (
+		testSubscription  = "test-sub"
+		testResourceGroup = "test-rg"
+		testResource      = "test-resource"
+	)
+
+	var testCases = []struct {
+		url          string
+		expectedType string
+		expectedName string
+	}{
+		{
+			url:          "/",
+			expectedName: "/",
+		},
+		{
+			url:          "/index.html",
+			expectedName: "/index.html",
+		},
+		{
+			url:          "/lib/bootstrap-4.5.2.min.css",
+			expectedName: "/lib/bootstrap-4.5.2.min.css",
+		},
+		{
+			url:          "/api/clusters",
+			expectedName: "/api/clusters",
+		},
+		{
+			url:          "/api/logout",
+			expectedName: "/api/logout",
+		},
+		{
+			url:          fmt.Sprintf("/subscriptions/%s/resourcegroups/%s/providers/microsoft.redhatopenshift/openshiftclusters/%s/ssh/new", testSubscription, testResourceGroup, testResource),
+			expectedName: fmt.Sprintf("/subscriptions/%s/resourcegroups/%s/providers/microsoft.redhatopenshift/openshiftclusters/%s/ssh/new", testSubscription, testResourceGroup, testResource),
+			expectedType: "ssh",
+		},
+		{
+			url:          fmt.Sprintf("/subscriptions/%s/resourcegroups/%s/providers/microsoft.redhatopenshift/openshiftclusters/%s/kubeconfig/new", testSubscription, testResourceGroup, testResource),
+			expectedName: fmt.Sprintf("/subscriptions/%s/resourcegroups/%s/providers/microsoft.redhatopenshift/openshiftclusters/%s/kubeconfig/new", testSubscription, testResourceGroup, testResource),
+			expectedType: "kubeconfig",
+		},
+		{
+			url:          fmt.Sprintf("/subscriptions/%s/resourcegroups/%s/providers/microsoft.redhatopenshift/openshiftclusters/%s/kubeconfig/proxy", testSubscription, testResourceGroup, testResource),
+			expectedName: fmt.Sprintf("/subscriptions/%s/resourcegroups/%s/providers/microsoft.redhatopenshift/openshiftclusters/%s/kubeconfig/proxy", testSubscription, testResourceGroup, testResource),
+			expectedType: "kubeconfig",
+		},
+		{
+			url:          fmt.Sprintf("/subscriptions/%s/resourcegroups/%s/providers/microsoft.redhatopenshift/openshiftclusters/%s/prometheus", testSubscription, testResourceGroup, testResource),
+			expectedName: fmt.Sprintf("/subscriptions/%s/resourcegroups/%s/providers/microsoft.redhatopenshift/openshiftclusters/%s/prometheus", testSubscription, testResourceGroup, testResource),
+			expectedType: "prometheus",
+		},
+	}
+
+	for _, tc := range testCases {
+		parsedURL, err := url.Parse(tc.url)
+		if err != nil {
+			t.Fatal("unexpected error: ", err)
+		}
+
+		r := &http.Request{URL: parsedURL}
+		if actual := auditTargetResourceType(r); tc.expectedType != actual {
+			t.Errorf("%s: expected: %s, actual: %s", tc.url, tc.expectedType, actual)
+		}
+
+		if actual := r.URL.Path; tc.expectedName != actual {
+			t.Errorf("%s: expected: %s, actual: %s", tc.url, tc.expectedName, actual)
+		}
+	}
 }
diff --git a/pkg/portal/security_test.go b/pkg/portal/security_test.go
index b0e704b09..ebc3d7ab1 100644
--- a/pkg/portal/security_test.go
+++ b/pkg/portal/security_test.go
@@ -9,21 +9,25 @@ import (
 	"crypto/x509"
 	"encoding/base64"
 	"encoding/json"
+	"fmt"
 	"net/http"
 	"strings"
 	"testing"
 	"time"
 
+	"github.com/Azure/go-autorest/autorest/azure"
 	"github.com/golang/mock/gomock"
 	"github.com/gorilla/securecookie"
 	"github.com/gorilla/sessions"
 	"github.com/sirupsen/logrus"
 
 	"github.com/Azure/ARO-RP/pkg/portal/middleware"
+	"github.com/Azure/ARO-RP/pkg/util/log/audit"
 	mock_env "github.com/Azure/ARO-RP/pkg/util/mocks/env"
 	utiltls "github.com/Azure/ARO-RP/pkg/util/tls"
 	testdatabase "github.com/Azure/ARO-RP/test/database"
 	"github.com/Azure/ARO-RP/test/util/listener"
+	testlog "github.com/Azure/ARO-RP/test/util/log"
 )
 
 var (
@@ -34,6 +38,10 @@ func TestSecurity(t *testing.T) {
 	ctx := context.Background()
 	log := logrus.NewEntry(logrus.StandardLogger())
 
+	_, portalAccessLog := testlog.New()
+	_, portalLog := testlog.New()
+	auditHook, portalAuditLog := testlog.NewAudit()
+
 	controller := gomock.NewController(t)
 	defer controller.Finish()
 
@@ -41,6 +49,8 @@ func TestSecurity(t *testing.T) {
 	_env.EXPECT().IsDevelopmentMode().AnyTimes().Return(false)
 	_env.EXPECT().Location().AnyTimes().Return("eastus")
 	_env.EXPECT().TenantID().AnyTimes().Return("00000000-0000-0000-0000-000000000001")
+	_env.EXPECT().Environment().AnyTimes().Return(&azure.PublicCloud)
+	_env.EXPECT().Hostname().AnyTimes().Return("testhost")
 
 	l := listener.NewListener()
 	defer l.Close()
@@ -76,7 +86,7 @@ func TestSecurity(t *testing.T) {
 		},
 	}
 
-	p := NewPortal(_env, log, log, l, sshl, nil, "", serverkey, servercerts, "", nil, nil, make([]byte, 32), sshkey, nil, elevatedGroupIDs, dbOpenShiftClusters, dbPortal, nil)
+	p := NewPortal(_env, portalAuditLog, portalLog, portalAccessLog, l, sshl, nil, "", serverkey, servercerts, "", nil, nil, make([]byte, 32), sshkey, nil, elevatedGroupIDs, dbOpenShiftClusters, dbPortal, nil)
 	go func() {
 		err := p.Run(ctx)
 		if err != nil {
@@ -90,24 +100,47 @@ func TestSecurity(t *testing.T) {
 		checkResponse                 func(*testing.T, bool, bool, *http.Response)
 		unauthenticatedWantStatusCode int
 		authenticatedWantStatusCode   int
+		wantAuditOperation            string
+		wantAuditTargetResources      []audit.TargetResource
 	}{
 		{
 			name: "/",
 			request: func() (*http.Request, error) {
 				return http.NewRequest(http.MethodGet, "https://server/", nil)
 			},
+			wantAuditOperation: "GET /",
+			wantAuditTargetResources: []audit.TargetResource{
+				{
+					TargetResourceType: "",
+					TargetResourceName: "/",
+				},
+			},
 		},
 		{
 			name: "/index.js",
 			request: func() (*http.Request, error) {
 				return http.NewRequest(http.MethodGet, "https://server/index.js", nil)
 			},
+			wantAuditOperation: "GET /index.js",
+			wantAuditTargetResources: []audit.TargetResource{
+				{
+					TargetResourceType: "",
+					TargetResourceName: "/index.js",
+				},
+			},
 		},
 		{
 			name: "/api/clusters",
 			request: func() (*http.Request, error) {
 				return http.NewRequest(http.MethodGet, "https://server/api/clusters", nil)
 			},
+			wantAuditOperation: "GET /api/clusters",
+			wantAuditTargetResources: []audit.TargetResource{
+				{
+					TargetResourceType: "",
+					TargetResourceName: "/api/clusters",
+				},
+			},
 		},
 		{
 			name: "/api/logout",
@@ -115,6 +148,13 @@ func TestSecurity(t *testing.T) {
 				return http.NewRequest(http.MethodPost, "https://server/api/logout", nil)
 			},
 			authenticatedWantStatusCode: http.StatusSeeOther,
+			wantAuditOperation:          "POST /api/logout",
+			wantAuditTargetResources: []audit.TargetResource{
+				{
+					TargetResourceType: "",
+					TargetResourceName: "/api/logout",
+				},
+			},
 		},
 		{
 			name: "/callback",
@@ -122,6 +162,13 @@ func TestSecurity(t *testing.T) {
 				return http.NewRequest(http.MethodGet, "https://server/callback", nil)
 			},
 			authenticatedWantStatusCode: http.StatusTemporaryRedirect,
+			wantAuditOperation:          "GET /callback",
+			wantAuditTargetResources: []audit.TargetResource{
+				{
+					TargetResourceType: "",
+					TargetResourceName: "/callback",
+				},
+			},
 		},
 		{
 			name: "/healthz/ready",
@@ -129,12 +176,26 @@ func TestSecurity(t *testing.T) {
 				return http.NewRequest(http.MethodGet, "https://server/healthz/ready", nil)
 			},
 			unauthenticatedWantStatusCode: http.StatusOK,
+			wantAuditOperation:            "GET /healthz/ready",
+			wantAuditTargetResources: []audit.TargetResource{
+				{
+					TargetResourceType: "",
+					TargetResourceName: "/healthz/ready",
+				},
+			},
 		},
 		{
 			name: "/kubeconfig/new",
 			request: func() (*http.Request, error) {
 				return http.NewRequest(http.MethodPost, "https://server/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourceGroupName/providers/microsoft.redhatopenshift/openshiftclusters/resourceName/kubeconfig/new", nil)
 			},
+			wantAuditOperation: "POST /subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourcegroupname/providers/microsoft.redhatopenshift/openshiftclusters/resourcename/kubeconfig/new",
+			wantAuditTargetResources: []audit.TargetResource{
+				{
+					TargetResourceType: "kubeconfig",
+					TargetResourceName: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourcegroupname/providers/microsoft.redhatopenshift/openshiftclusters/resourcename/kubeconfig/new",
+				},
+			},
 		},
 		{
 			name: "/prometheus",
@@ -142,6 +203,13 @@ func TestSecurity(t *testing.T) {
 				return http.NewRequest(http.MethodPost, "https://server/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourceGroupName/providers/microsoft.redhatopenshift/openshiftclusters/resourceName/prometheus", nil)
 			},
 			authenticatedWantStatusCode: http.StatusTemporaryRedirect,
+			wantAuditOperation:          "POST /subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourcegroupname/providers/microsoft.redhatopenshift/openshiftclusters/resourcename/prometheus",
+			wantAuditTargetResources: []audit.TargetResource{
+				{
+					TargetResourceType: "prometheus",
+					TargetResourceName: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourcegroupname/providers/microsoft.redhatopenshift/openshiftclusters/resourcename/prometheus",
+				},
+			},
 		},
 		{
 			name: "/ssh/new",
@@ -168,6 +236,13 @@ func TestSecurity(t *testing.T) {
 					}
 				}
 			},
+			wantAuditOperation: "POST /subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourcegroupname/providers/microsoft.redhatopenshift/openshiftclusters/resourcename/ssh/new",
+			wantAuditTargetResources: []audit.TargetResource{
+				{
+					TargetResourceType: "ssh",
+					TargetResourceName: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourcegroupname/providers/microsoft.redhatopenshift/openshiftclusters/resourcename/ssh/new",
+				},
+			},
 		},
 		{
 			name: "/doesnotexist",
@@ -201,6 +276,8 @@ func TestSecurity(t *testing.T) {
 			},
 		} {
 			t.Run(tt2.name+tt.name, func(t *testing.T) {
+				defer auditHook.Reset()
+
 				req, err := tt.request()
 				if err != nil {
 					t.Fatal(err)
@@ -243,6 +320,36 @@ func TestSecurity(t *testing.T) {
 				if tt.checkResponse != nil {
 					tt.checkResponse(t, tt2.authenticated, tt2.elevated, resp)
 				}
+
+				// no audit logs for https://server/doesnotexist
+				if tt.authenticatedWantStatusCode == http.StatusNotFound {
+					return
+				}
+
+				// skipping https://server because the http.ServeContent() calls in the
+				// portal's serve() and index() handlers[1] issued a call to io.Copy()[2]
+				// causes a race condition with the audit hook. The response was returned
+				// to the client and the testlog.AssertAuditPayloads() was called immediately,
+				// while the audit hook was still in-flight.
+				//
+				// note that the audit logs will still be recorded and emitted by the audit
+				// hook, so this is a non-issue in the Geneva environment.
+				//
+				// [1] https://github.com/Azure/ARO-RP/blob/master/pkg/portal/portal.go#L222-L247
+				// [2] https://go.googlesource.com/go/+/go1.16.2/src/net/http/fs.go#337
+				if tt.name == "/" || tt.name == "/index.js" {
+					return
+				}
+
+				payload := auditPayloadFixture()
+				payload.OperationName = tt.wantAuditOperation
+				payload.TargetResources = tt.wantAuditTargetResources
+				payload.Result.ResultDescription = fmt.Sprintf("Status code: %d", tt2.wantStatusCode)
+				if tt2.authenticated && tt.name != "/callback" && tt.name != "/healthz/ready" {
+					payload.CallerIdentities[0].CallerIdentityValue = "username"
+				}
+
+				testlog.AssertAuditPayloads(t, auditHook, []*audit.Payload{payload})
 			})
 		}
 	}
@@ -282,3 +389,29 @@ func addAuth(req *http.Request, groups []string) error {
 
 	return nil
 }
+
+func auditPayloadFixture() *audit.Payload {
+	return &audit.Payload{
+		EnvVer:               audit.IFXAuditVersion,
+		EnvName:              audit.IFXAuditName,
+		EnvFlags:             257,
+		EnvAppID:             audit.SourceAdminPortal,
+		EnvCloudName:         azure.PublicCloud.Name,
+		EnvCloudRole:         audit.CloudRoleRP,
+		EnvCloudRoleInstance: "testhost",
+		EnvCloudEnvironment:  azure.PublicCloud.Name,
+		EnvCloudLocation:     "eastus",
+		EnvCloudVer:          1,
+		CallerIdentities: []audit.CallerIdentity{
+			{
+				CallerDisplayName:  "",
+				CallerIdentityType: audit.CallerIdentityTypeUsername,
+				CallerIPAddress:    "bufferedpipe",
+			},
+		},
+		Category: audit.CategoryResourceManagement,
+		Result: audit.Result{
+			ResultType: audit.ResultTypeSuccess,
+		},
+	}
+}