Compare commits
2 Commits
8e58ee85f0
...
3c53a6a33e
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c53a6a33e | |||
| e5949c3967 |
30
.github/workflows/deploy.yml
vendored
30
.github/workflows/deploy.yml
vendored
@@ -82,6 +82,36 @@ jobs:
|
||||
printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts
|
||||
chmod 644 ~/.ssh/known_hosts
|
||||
|
||||
- name: Reconcile tenant API domains
|
||||
env:
|
||||
HOST: ${{ secrets.DEPLOY_HOST }}
|
||||
USER: ${{ secrets.DEPLOY_USER }}
|
||||
STOREFRONT_DOMAINS: ${{ secrets.STOREFRONT_DOMAINS }}
|
||||
CERTBOT_EMAIL: ${{ secrets.CERTBOT_EMAIL }}
|
||||
BACKEND_UPSTREAM: ${{ secrets.BACKEND_UPSTREAM }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -n "$HOST" || { echo "secret DEPLOY_HOST is empty" >&2; exit 1; }
|
||||
test -n "$USER" || { echo "secret DEPLOY_USER is empty" >&2; exit 1; }
|
||||
test -n "$STOREFRONT_DOMAINS" || { echo "secret STOREFRONT_DOMAINS is empty" >&2; exit 1; }
|
||||
test -n "$CERTBOT_EMAIL" || { echo "secret CERTBOT_EMAIL is empty" >&2; exit 1; }
|
||||
BACKEND_UPSTREAM="${BACKEND_UPSTREAM:-https://127.0.0.1:445}"
|
||||
[[ "$CERTBOT_EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]] || {
|
||||
echo "CERTBOT_EMAIL is invalid" >&2; exit 1;
|
||||
}
|
||||
[[ "$BACKEND_UPSTREAM" =~ ^https?://[A-Za-z0-9.:-]+$ ]] || {
|
||||
echo "BACKEND_UPSTREAM is invalid" >&2; exit 1;
|
||||
}
|
||||
|
||||
SSH="ssh -i ~/.ssh/deploy_key -o BatchMode=yes"
|
||||
for domain in $STOREFRONT_DOMAINS; do
|
||||
[[ "$domain" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$ ]] || {
|
||||
echo "invalid storefront domain: $domain" >&2; exit 1;
|
||||
}
|
||||
$SSH "$USER@$HOST" sudo /usr/local/sbin/marketplaces-configure-api-domain \
|
||||
--domain "$domain" --email "$CERTBOT_EMAIL" --upstream "$BACKEND_UPSTREAM"
|
||||
done
|
||||
|
||||
- name: Upload release
|
||||
env:
|
||||
HOST: ${{ secrets.DEPLOY_HOST }}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Deployment — server provisioning, CD, TLS
|
||||
|
||||
Frontend only. The backend service (`:8080`) is a separate developer's responsibility. API hostnames are separate reverse proxies and will return `502` until their upstream exists.
|
||||
Frontend deployment plus API-domain edge configuration. The backend service is a
|
||||
separate developer's responsibility. API hostnames are separate reverse proxies
|
||||
and return `502` until their configured upstream exists (production currently
|
||||
defaults to `https://127.0.0.1:445`).
|
||||
|
||||
**Multi-tenant, one bundle.** Every customer domain is served by the same build. The SPA resolves its tenant from the `Host` header ([BACKEND-HANDOFF §1a](backend/BACKEND-HANDOFF.md)). One deploy updates every domain simultaneously — there is no per-tenant build and no per-tenant deploy.
|
||||
|
||||
@@ -18,6 +21,7 @@ nested names such as `api.store1.example.com`.
|
||||
|---|---|
|
||||
| `scripts/deploy/server-setup.sh` | One-time server provisioning. Idempotent. Run as root. |
|
||||
| `scripts/deploy/add-domain.sh` | Attach one domain + issue TLS. Run per domain, as root, after DNS resolves. |
|
||||
| `scripts/deploy/configure-api-domain.sh` | Configure `api.<full storefront host>` TLS, exact CORS, backend proxy, and JSON bootstrap verification. |
|
||||
| `.github/workflows/deploy.yml` | CD: build → upload → atomic swap → verify. Triggers on push to `main`. |
|
||||
|
||||
---
|
||||
@@ -56,7 +60,10 @@ Copy `scripts/deploy/` to the server and run:
|
||||
sudo bash server-setup.sh --pubkey "$(cat marketplaces_deploy.pub)"
|
||||
```
|
||||
|
||||
This installs nginx + certbot, creates a **key-only** `deploy` user with no password, writes the catch-all nginx config, opens 80/443/OpenSSH in ufw, and grants `deploy` exactly one sudo right: `systemctl reload nginx`.
|
||||
This installs nginx + certbot, creates a **key-only** `deploy` user with no
|
||||
password, writes the catch-all nginx config, opens 80/443/OpenSSH in ufw, and
|
||||
installs a root-owned, argument-validating API-domain helper. The deploy user may
|
||||
run that helper and reload nginx, but cannot replace the helper.
|
||||
|
||||
Verify before continuing:
|
||||
|
||||
@@ -82,6 +89,15 @@ The output is the `DEPLOY_KNOWN_HOSTS` secret. Pinning it means a rebuilt or imp
|
||||
| `DEPLOY_USER` | `deploy` |
|
||||
| `DEPLOY_SSH_KEY` | contents of the **private** key file |
|
||||
| `DEPLOY_KNOWN_HOSTS` | output of `ssh-keyscan -H <server-ip>` |
|
||||
| `STOREFRONT_DOMAINS` | space-separated full hosts, e.g. `gorbushka.market store1.example.com` |
|
||||
| `CERTBOT_EMAIL` | operations email used for Let's Encrypt |
|
||||
| `BACKEND_UPSTREAM` | optional; defaults to `https://127.0.0.1:445` |
|
||||
|
||||
Before deploying, point every derived API hostname at the server. For the
|
||||
example above, DNS must resolve both `api.gorbushka.market` and
|
||||
`api.store1.example.com`. The workflow deliberately stops before release
|
||||
activation if DNS, certificate issuance, nginx validation, or the JSON
|
||||
`/bootstrap` check fails.
|
||||
|
||||
### 3.5 Deploy
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ Single entry point for a backend developer picking this up cold. Written 2026-08
|
||||
|
||||
## 1a. Multi-tenancy — the thing that shapes every endpoint
|
||||
|
||||
The final executable infrastructure/backend contract is
|
||||
[TENANT-API-DOMAIN-HANDOFF.md](TENANT-API-DOMAIN-HANDOFF.md). Follow it for
|
||||
hostname normalization, CORS, nginx, TLS, CI secrets, and acceptance checks.
|
||||
|
||||
One deployed bundle serves **every customer domain**. There is no per-tenant build. The chain is:
|
||||
|
||||
1. [`TenantResolverService`](../../src/app/core/config/tenant-resolver.service.ts) reads the complete current browser hostname and protocol (localhost still uses the development proxy).
|
||||
@@ -87,7 +91,12 @@ npm install # pulls @marketplaces/auth over git, no credentials needed
|
||||
npm run build # -> dist/dexarmarket
|
||||
```
|
||||
|
||||
Angular 22, Node 20+. nginx serves `/srv/marketplaces/current/frontend`, so deploying means copying `dist/dexarmarket` there — **manually, today.** There is no CD pipeline. Because of the multi-tenant design (§1a), one such deploy updates every domain at once.
|
||||
Angular 22, Node 24+. nginx serves `/srv/marketplaces/current/frontend`.
|
||||
[`deploy.yml`](../../.github/workflows/deploy.yml) builds and atomically deploys
|
||||
pushes to `main`; one deployment updates every domain at once. Before activation,
|
||||
the workflow reconciles TLS, exact CORS, and reverse proxying for every host in
|
||||
`STOREFRONT_DOMAINS`. Production deployment requires the documented CI secrets
|
||||
and the one-time [`server-setup.sh`](../../scripts/deploy/server-setup.sh) run.
|
||||
|
||||
## 7. Known open decisions
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
> **New here? Start with [BACKEND-HANDOFF.md](BACKEND-HANDOFF.md)** — reading order, current infrastructure state, auth surface, and what a working dev environment still needs.
|
||||
>
|
||||
> **Implementing tenant routing/nginx? [TENANT-API-DOMAIN-HANDOFF.md](TENANT-API-DOMAIN-HANDOFF.md)** is the final host normalization, CORS, TLS, reverse-proxy, CI-secret, and acceptance contract.
|
||||
>
|
||||
> **Want every endpoint in one place? [FRONTEND-API-SURFACE-COMPLETE.md](FRONTEND-API-SURFACE-COMPLETE.md)** — the final handoff doc. Generated directly from source, all 90 endpoints the frontend currently calls plus 3 response-shape additions on existing endpoints, marked Specified / Inferred / Undocumented against the contracts below. Use it to see gaps across all contracts at once; use the individual Phase/Track docs for full entity shapes and invariants.
|
||||
|
||||
This directory is the complete set of wire contracts for building the backend behind [Product Plan v3.1](../PRODUCT-PLAN-v3.1-GAP-ANALYSIS.md). Each doc specifies entities, endpoints, and invariants only — never DB schema or service boundaries, which stay backend's own call.
|
||||
|
||||
124
docs/backend/TENANT-API-DOMAIN-HANDOFF.md
Normal file
124
docs/backend/TENANT-API-DOMAIN-HANDOFF.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Final handoff: tenant API domains
|
||||
|
||||
This is the required production contract between the shared frontend, nginx,
|
||||
and the backend. It supersedes any fixed `api.dexarmarket.ru` or same-origin
|
||||
`/backend` routing proposal.
|
||||
|
||||
## 1. Deterministic hostname rule
|
||||
|
||||
The frontend prefixes the **complete** storefront hostname with `api.`:
|
||||
|
||||
| Storefront | API origin | Bootstrap |
|
||||
|---|---|---|
|
||||
| `example.com` | `https://api.example.com` | `https://api.example.com/bootstrap` |
|
||||
| `store1.example.com` | `https://api.store1.example.com` | `https://api.store1.example.com/bootstrap` |
|
||||
| `www.example.com` | `https://api.www.example.com` | `https://api.www.example.com/bootstrap` |
|
||||
|
||||
Bootstrap, auth, legacy routes, and `/api/...` routes all use this origin.
|
||||
Localhost is the only exception and continues through the local `/api` proxy.
|
||||
|
||||
## 2. Backend changes required
|
||||
|
||||
For every request received publicly on `api.<storefront-host>`:
|
||||
|
||||
1. Behind the trusted project nginx, use `X-Storefront-Host`. nginx deliberately
|
||||
sends the same storefront value as upstream `Host` for compatibility with the
|
||||
currently live backend and preserves the public API hostname in
|
||||
`X-Forwarded-Host`.
|
||||
2. Without that trusted proxy, normalize the request `Host`: lowercase, remove
|
||||
the port, remove exactly one leading `api.` label when present, and retain
|
||||
every remaining label.
|
||||
3. Resolve that normalized storefront hostname through the tenant-domain
|
||||
registry. Do not infer a tenant from only the first label.
|
||||
4. Reject unknown, disabled, or unverified domains with `403` before reading
|
||||
tenant data. Never fall back to the default/Dexar tenant.
|
||||
5. Bind the authenticated session to the resolved tenant and reject a mismatch.
|
||||
6. Trust `X-Storefront-Host` / `X-Forwarded-*` only from the known nginx proxy;
|
||||
direct clients can forge them.
|
||||
7. Return JSON for `/bootstrap`, including a tenant identity that matches the
|
||||
normalized storefront domain. HTML or a default tenant response is a fault.
|
||||
|
||||
Pseudo-code:
|
||||
|
||||
```text
|
||||
if request.remoteAddress is trustedProxy:
|
||||
storefrontHost = lower(stripPort(request.header["X-Storefront-Host"]))
|
||||
else:
|
||||
requestHost = lower(stripPort(request.host))
|
||||
storefrontHost = removeAtMostOnePrefix(requestHost, "api.")
|
||||
tenant = registry.findVerifiedDomain(storefrontHost) ?? forbidden()
|
||||
request.tenant = tenant
|
||||
```
|
||||
|
||||
## 3. CORS contract
|
||||
|
||||
For API host `api.<storefront-host>`, allow exactly:
|
||||
|
||||
```http
|
||||
Access-Control-Allow-Origin: https://<storefront-host>
|
||||
Access-Control-Allow-Credentials: true
|
||||
Vary: Origin
|
||||
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
|
||||
Access-Control-Allow-Headers: Authorization, Content-Type, AdminWebSessionID, X-Requested-With
|
||||
```
|
||||
|
||||
Answer valid preflight requests with `204`. Do not use `*` together with
|
||||
credentials. nginx applies this policy now; the backend should enforce the same
|
||||
allowlist when it is reached without that proxy.
|
||||
|
||||
## 4. nginx and TLS
|
||||
|
||||
Run the idempotent project script as root:
|
||||
|
||||
```bash
|
||||
scripts/deploy/configure-api-domain.sh \
|
||||
--domain gorbushka.market \
|
||||
--email ops@example.com \
|
||||
--upstream https://127.0.0.1:445
|
||||
```
|
||||
|
||||
It creates `api.gorbushka.market`, issues/renews its certificate, configures
|
||||
CORS, and proxies all paths to the backend. Upstream receives
|
||||
`Host: gorbushka.market`, `X-Forwarded-Host: api.gorbushka.market`, and
|
||||
`X-Storefront-Host: gorbushka.market`; the script then reloads nginx and verifies
|
||||
that `/bootstrap` returns a JSON object.
|
||||
|
||||
For `store1.example.com`, both DNS and TLS must exist for
|
||||
`api.store1.example.com`. A certificate for `*.example.com` does **not** cover
|
||||
that two-label-deep hostname.
|
||||
|
||||
## 5. CI/CD contract
|
||||
|
||||
`deploy.yml` runs the same root-owned configurator before activating a frontend
|
||||
release. Required production secrets:
|
||||
|
||||
| Secret | Example |
|
||||
|---|---|
|
||||
| `DEPLOY_HOST` | server hostname/IP |
|
||||
| `DEPLOY_USER` | `deploy` |
|
||||
| `DEPLOY_SSH_KEY` | private deploy key |
|
||||
| `DEPLOY_KNOWN_HOSTS` | pinned SSH host-key line |
|
||||
| `STOREFRONT_DOMAINS` | `gorbushka.market store1.example.com` |
|
||||
| `CERTBOT_EMAIL` | operations email |
|
||||
| `BACKEND_UPSTREAM` | `https://127.0.0.1:445` (optional default) |
|
||||
|
||||
One-time provisioning must first run `server-setup.sh`; it installs the helper
|
||||
as root-owned `/usr/local/sbin/marketplaces-configure-api-domain` and grants the
|
||||
deploy user permission to run only that validated command plus nginx reload.
|
||||
|
||||
## 6. Acceptance checks
|
||||
|
||||
For every storefront domain, all of these must pass:
|
||||
|
||||
```bash
|
||||
curl -fsS https://api.example.com/bootstrap | jq -e 'type == "object"'
|
||||
curl -i -X OPTIONS https://api.example.com/bootstrap \
|
||||
-H 'Origin: https://example.com' \
|
||||
-H 'Access-Control-Request-Method: GET'
|
||||
```
|
||||
|
||||
- Frontend bundle contains no fixed marketplace API hostname.
|
||||
- Root and nested storefronts call their matching `api.<full-hostname>`.
|
||||
- Unknown API hosts return `403`, not the default tenant.
|
||||
- `/bootstrap` returns JSON and the correct tenant.
|
||||
- API responses never return the Angular `index.html` fallback.
|
||||
@@ -115,6 +115,17 @@ certbot --nginx "${CERT_ARGS[@]}" \
|
||||
nginx -t
|
||||
systemctl reload nginx
|
||||
|
||||
echo "==> companion API domain(s)"
|
||||
CONFIGURE_API="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/configure-api-domain.sh"
|
||||
[[ -x "$CONFIGURE_API" ]] || {
|
||||
echo "ERROR: configure-api-domain.sh must be executable and next to add-domain.sh" >&2
|
||||
exit 1
|
||||
}
|
||||
"$CONFIGURE_API" --domain "$DOMAIN" --email "$EMAIL"
|
||||
if [[ $WITH_WWW -eq 1 ]]; then
|
||||
"$CONFIGURE_API" --domain "www.$DOMAIN" --email "$EMAIL"
|
||||
fi
|
||||
|
||||
echo "==> renewal timer"
|
||||
systemctl enable --now certbot.timer
|
||||
systemctl status certbot.timer --no-pager | head -3 || true
|
||||
|
||||
107
scripts/deploy/configure-api-domain.sh
Executable file
107
scripts/deploy/configure-api-domain.sh
Executable file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
# Configure api.<storefront-domain> as the TLS/CORS reverse proxy for one tenant.
|
||||
# Idempotent. Run as root after both storefront and API DNS records resolve here.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DOMAIN=""
|
||||
EMAIL=""
|
||||
UPSTREAM="https://127.0.0.1:445"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--domain) DOMAIN="$2"; shift 2 ;;
|
||||
--email) EMAIL="$2"; shift 2 ;;
|
||||
--upstream) UPSTREAM="$2"; shift 2 ;;
|
||||
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ $EUID -eq 0 ]] || { echo "must run as root" >&2; exit 1; }
|
||||
[[ "$DOMAIN" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$ ]] || {
|
||||
echo "--domain must be a valid lowercase hostname" >&2; exit 2;
|
||||
}
|
||||
[[ "$EMAIL" =~ ^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$ ]] || {
|
||||
echo "--email must be valid" >&2; exit 2;
|
||||
}
|
||||
[[ "$UPSTREAM" =~ ^https?://[a-zA-Z0-9.:-]+$ ]] || {
|
||||
echo "--upstream must be an http(s) origin without a path" >&2; exit 2;
|
||||
}
|
||||
|
||||
API_DOMAIN="api.$DOMAIN"
|
||||
CONF="/etc/nginx/sites-available/$API_DOMAIN"
|
||||
|
||||
echo "==> checking DNS for $API_DOMAIN"
|
||||
getent hosts "$API_DOMAIN" >/dev/null || {
|
||||
echo "ERROR: $API_DOMAIN does not resolve; create DNS before provisioning TLS" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cat > "$CONF" <<NGINX
|
||||
# Managed by marketplaces configure-api-domain.sh. Manual edits are overwritten.
|
||||
# Storefront $DOMAIN derives this API origin as https://$API_DOMAIN.
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name $API_DOMAIN;
|
||||
|
||||
access_log /var/log/nginx/$API_DOMAIN.access.log;
|
||||
error_log /var/log/nginx/$API_DOMAIN.error.log;
|
||||
|
||||
set \$cors_origin "";
|
||||
if (\$http_origin = "https://$DOMAIN") { set \$cors_origin \$http_origin; }
|
||||
|
||||
add_header Access-Control-Allow-Origin \$cors_origin always;
|
||||
add_header Access-Control-Allow-Credentials "true" always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
|
||||
add_header Access-Control-Allow-Headers "Authorization, Content-Type, AdminWebSessionID, X-Requested-With" always;
|
||||
add_header Vary "Origin" always;
|
||||
|
||||
if (\$request_method = OPTIONS) { return 204; }
|
||||
|
||||
location / {
|
||||
proxy_pass $UPSTREAM;
|
||||
proxy_http_version 1.1;
|
||||
# Keep the existing backend compatible: it already serves this tenant
|
||||
# when the storefront Host reaches :445. The original public API host
|
||||
# remains available in the trusted forwarding headers below.
|
||||
proxy_set_header Host $DOMAIN;
|
||||
proxy_set_header X-Forwarded-Host $API_DOMAIN;
|
||||
proxy_set_header X-Storefront-Host $DOMAIN;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_read_timeout 60s;
|
||||
proxy_connect_timeout 10s;
|
||||
|
||||
proxy_ssl_server_name on;
|
||||
proxy_ssl_name $DOMAIN;
|
||||
}
|
||||
}
|
||||
NGINX
|
||||
|
||||
ln -sfn "$CONF" "/etc/nginx/sites-enabled/$API_DOMAIN"
|
||||
nginx -t
|
||||
|
||||
certbot --nginx -d "$API_DOMAIN" \
|
||||
--non-interactive --agree-tos --email "$EMAIL" \
|
||||
--redirect --keep-until-expiring
|
||||
|
||||
nginx -t
|
||||
systemctl reload nginx
|
||||
|
||||
echo "==> verifying https://$API_DOMAIN/bootstrap"
|
||||
bootstrap_tmp="$(mktemp)"
|
||||
trap 'rm -f "$bootstrap_tmp"' EXIT
|
||||
content_type="$(curl --resolve "$API_DOMAIN:443:127.0.0.1" -fsS \
|
||||
-o "$bootstrap_tmp" -w '%{content_type}' \
|
||||
"https://$API_DOMAIN/bootstrap")"
|
||||
[[ "$content_type" == application/json* ]] || {
|
||||
echo "ERROR: $API_DOMAIN/bootstrap returned $content_type, expected application/json" >&2
|
||||
exit 1
|
||||
}
|
||||
jq -e 'type == "object"' "$bootstrap_tmp" >/dev/null
|
||||
rm -f "$bootstrap_tmp"
|
||||
trap - EXIT
|
||||
|
||||
echo "configured: $DOMAIN -> https://$API_DOMAIN -> $UPSTREAM"
|
||||
@@ -133,8 +133,17 @@ nginx -t
|
||||
systemctl enable --now nginx
|
||||
systemctl reload nginx
|
||||
|
||||
echo "==> dynamic domain reconciler"
|
||||
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
echo "==> tenant API-domain configurator"
|
||||
if [[ -f "$SRC_DIR/configure-api-domain.sh" ]]; then
|
||||
install -m 755 -o root -g root "$SRC_DIR/configure-api-domain.sh" \
|
||||
/usr/local/sbin/marketplaces-configure-api-domain
|
||||
else
|
||||
echo "configure-api-domain.sh not found next to server-setup.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> dynamic domain reconciler"
|
||||
install -d -m 755 "$BASE/bin" /etc/marketplaces "/var/lib/marketplaces"
|
||||
if [[ -f "$SRC_DIR/sync-domains.sh" ]]; then
|
||||
install -m 755 "$SRC_DIR/sync-domains.sh" "$BASE/bin/sync-domains.sh"
|
||||
@@ -172,9 +181,10 @@ else
|
||||
echo " sync-domains.sh not found next to this script - skipping"
|
||||
fi
|
||||
|
||||
echo "==> sudoers: let the deploy user reload nginx, nothing else"
|
||||
echo "==> sudoers: deployment reload plus validated tenant API provisioning"
|
||||
cat > /etc/sudoers.d/marketplaces-deploy <<SUDO
|
||||
$DEPLOY_USER ALL=(root) NOPASSWD: /bin/systemctl reload nginx
|
||||
Cmnd_Alias MARKETPLACES_DEPLOY = /bin/systemctl reload nginx, /usr/local/sbin/marketplaces-configure-api-domain *
|
||||
$DEPLOY_USER ALL=(root) NOPASSWD: MARKETPLACES_DEPLOY
|
||||
SUDO
|
||||
chmod 440 /etc/sudoers.d/marketplaces-deploy
|
||||
visudo -c -f /etc/sudoers.d/marketplaces-deploy
|
||||
|
||||
Reference in New Issue
Block a user